SE4Binome2024-6

De projets-se.plil.fr
Révision datée du 30 janvier 2025 à 21:01 par Rex (discussion | contributions) (→‎Test de la carte fille)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigation Aller à la recherche

GIT

Nos codes et nos conceptions kicad sont disponibles via notre GIT : https://gitea.plil.fr/yyahiani/pico_yahiani_zongo.git

Objectif

Nous avons pour objectif, avec les trois autres binômes de notre groupe, de construire un pico-ordinateur qui intégrera plusieurs éléments essentiels. Voici les composants que nous allons inclure :

  • Un processeur de type microcontrôleur : Cela constituera le cœur de notre pico-ordinateur et permettra de gérer toutes les opérations.
  • Un clavier : Il permettra l'entrée de données et d'interagir facilement avec notre dispositif.
  • Un dispositif d'affichage : Cela nous permettra de visualiser les informations ainsi que les résultats des opérations effectuées par notre pico-ordinateur.
  • Un système d'exploitation : Il sera stocké dans la mémoire flash du microcontrôleur, garantissant un fonctionnement fluide de l'appareil.
  • Une mémoire de masse : Nous prévoyons d'ajouter une mémoire de masse pour stocker davantage de données.
  • Un dispositif de communication externe : Cela permettra d'interagir avec d'autres dispositifs ou réseaux.

Enfin, pour assurer la communication entre ces éléments, nous mettrons en place un bus série. Cela facilitera les échanges de données et garantira une intégration harmonieuse des composants.

Notre binôme se concentrera sur la réalisation d'une carte écran, qui devra remplir les tâches précisées dans l'énoncé du projet.

En premier lieu, nous avons d'abord réalisé un shield qui nous servira, dans le cas où la carte mère ne serait pas fonctionnelle, à tester nos cartes filles à l'aide d'un Arduino Uno pour prototyper le cœur de la carte mère.

Shield

Realisation

Nous nous sommes aidés des indications données par nos encadrants lors de la première séance pour réaliser le schéma de notre shield. Nous y avons ajouté une puce mémoire AT45DB161D, et la datasheet nous a permis de comprendre comment l'intégrer correctement dans notre schéma. Vous pouvez voir ci-dessous la version finale du routage du PCB du bouclier ainsi que ce dernier après la soudure des différents composants.

schematique du Bouclier
PCB routé
Carte après soudure


Suite à une erreur d'orientation de nos broches, connecter l'Arduino et notre bouclier est devenue une tâche bien plus "compliquée" que prévu. Cependant, grâce à une idée ingénieuse proposée par nos encadrants (M. BOE et M. REDON), nous y sommes parvenus. Le bouclier se connecte donc à l'Arduino de la manière suivante :

connexion Bouclier-Arduino
connexion Bouclier-Arduino

Tests

LEDs

Après avoir connecté notre bouclier et l'Arduino nous avons effectué les tests et constaté l'allumage des différentes LEDs; attestant du bon fonctionnement de notre bouclier.

Voici le code C (GIT : pico_yahiani_zongo/Software/codes_Shield/clignotement) qui nous permet de faire clignoter les LEDs.

Code C Vidéo fonctionnement

#define BROCHE_LED_BLEU 4
#define BROCHE_LED_ORANGE 7
#define BROCHE_LED_ROUGE 14
#define BROCHE_LED_JAUNE 17
#define BROCHE_D3 0

void setup()
{
  pinMode(BROCHE_LED_BLEU, OUTPUT);
  pinMode(BROCHE_LED_JAUNE, OUTPUT);
  pinMode(BROCHE_LED_ROUGE, OUTPUT);
  pinMode(BROCHE_LED_ORANGE, OUTPUT);
  pinMode(BROCHE_D3, OUTPUT) ;
}

void loop()
{
  digitalWrite(BROCHE_D3, HIGH);
  digitalWrite(BROCHE_LED_ORANGE, HIGH);
  delay (1000);
  digitalWrite(BROCHE_LED_ROUGE, HIGH);
  delay(1000);
  digitalWrite (BROCHE_LED_BLEU, HIGH);
  delay (1000);
  digitalWrite(BROCHE_LED_JAUNE, HIGH);
  delay (1000) ;
  digitalWrite(BROCHE_LED_ROUGE, LOW);
  digitalWrite(BROCHE_LED_BLEU, LOW);
  digitalWrite(BROCHE_LED_JAUNE, LOW);
  digitalWrite(BROCHE_LED_ORANGE, LOW);
  delay(1000);
}

fonctionnement effectif du bouclier (LED allumées)
fonctionnement effectif du bouclier (LED allumées)

Puce mémoire

Après avoir testé les LEDs, nous cherchons à tester notre puce mémoire pour assurer son bon fonctionnement. Cependant, nous avons rencontré un petit problème.

Problème

En effet, lors du test de notre puce mémoire, nous avons introduit une carte SD pour vérifier, via le logiciel Arduino, si notre puce mémoire détecte la carte SD. Cependant, cela fut sans succès. Nous avons donc vérifié l'horloge de notre Arduino Uno à l'aide d'un oscilloscope en y injectant ce code (GIT : pico_yahiani_zongo/Software/codes_Shield/test_clk_arduino) :

Code arduino Visualisation

#include <SPI.h>

// set pin 10 as the slave select for the digital pot:
const int slaveSelectPin = 10;

void setup()
{
  // set the slaveSelectPin as an output:
  Serial.begin (9600);
  pinMode(slaveSelectPin, OUTPUT),
  // initialize SPI:
  SPI.begin();
}

void loop()
{
  
  digitalWrite(slaveSelectPin, LOW);
  SPI.transfer(0xaa);
  Serial.write("ok");
  delay(100);
}

Signal d'horloge

Le signal d'horloge de notre Arduino est correct.

Le problème ne venant pas de l'Arduino, nous avons donc dessoudé puis ressoudé notre puce mémoire et le problème à été résolu, comme vous pouvez le voir ci-dessous.

Carte SD reconnue

Connecteurs HE10

À l'aide d'un afficheur 7 segments, nous vérifions le bon fonctionnement des connecteurs HE10 en affichant "GrP6" pour Groupe 6. Pour cela, nous configurons la communication SPI avec avr-gcc grâce au code présent dans notre fichier spi.c, grandement inspiré du code disponible sur le site de M. Redon (https://rex.plil.fr/Enseignement/Systeme/Systeme.PSE/systeme.html). Ci-dessous, vous trouverez un aperçu du code source utilisé pour gérer la communication SPI ainsi que celui utilisé pour gérer l'affichage du 7 segments.

Configuration communication SPI

#define SPI_DDR         DDRB
#define SS_DDR          DDRC
#define SPI_PORT        PORTC

#define SPI_SS_M        2 // 
#define SPI_MOSI        3
#define SPI_MISO        4
#define SPI_SCK         5

void spi_activer(void){                              // Activer le périphérique
SPI_PORT &= ~(1<<0);                            // Ligne SS à l'état bas
}

void spi_desactiver(void){                           // Désactiver le périphérique
SPI_PORT |= (1<<0);                             // 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 spi_init(void){                                 // Initialisation du bus SPI
SPI_DDR |= (1<<SPI_MOSI)|(1<<SPI_SCK)|(1<<SPI_SS_M);   // Définition des sorties
SPI_DDR &= ~(1<<SPI_MISO);                           // Définition de l'entrée
                             // Désactivation du périphérique
SS_DDR |= (1<<0); // afficheur sur pc0
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR1);                 // Activation SPI (SPE) en état maître (MSTR)
SPI_PORT |= (1<<0);                                                   // horloge F_CPU/64 (SPR1=1,SPR0=0)
}

Code de gestion du sept segments Image de fonctionnement
#include <avr/io.h>
#include <avr/interrupt.h>  
#include <util/delay.h>
#include "SPI.h"

int main(void){
spi_init();
spi_activer();
    spi_echange(0x76); // adresse de l'afficheur du 7 segment
    spi_desactiver();

while(1){
         spi_activer(); // active 
         spi_echange('G');
         _delay_ms(5);
         spi_echange('R');
         _delay_ms(5);
         spi_echange('p');
         _delay_ms(5);
         spi_echange('6');

         spi_desactiver(); //désactive
         _delay_ms(5);            
       }
return 0;
}

connecteur HE10 fonctionnel

Ordonnanceur

Clignotement de deux LEDS

La principale tâche à accomplir en premier lieu est de faire clignoter deux LEDs à des fréquences différentes. Pour cela nous passerons par plusieurs étapes. La première tâche à réaliser est de programmer le minuteur 1 de l'ATMega328p de sorte à ce qu'il génère une interruption toutes les 20ms. Pour cela nous nous sommes inspirés du code donné par l'un de nos encadrants (M. REDON) et avons programmé le minuteur 1 comme vous pouvez le voir dans notre Git : pico_yahiani_zongo/Software/codes_Shield/ISR_nonNue_1tache/timer.c.

Le Timer 1 est configuré en mode CTC, c'est-à-dire qu'il se réinitialise automatiquement à chaque fois qu'il atteint la valeur OCR1A. Le prescaler est configuré à 256, ce qui divise la fréquence de l'horloge système (16000000 Hz) par 256. Nous avons donc une nouvelle fréquence d'horloge à 62500 Hz, ce qui implique que les coups d'horloge (ticks) s'effectuent toutes les 16 µs. Le timer compte jusqu'à 1250 ticks, ce qui génère une interruption toutes les 20 ms.

La seconde étape consistait à faire clignoter la LED sur la broche Arduino 13 (PB5) en utilisant une ISR non nue avec notre timer. Le code suivant nous a permis de réaliser cette tâche.

Code C Vidéo de fonctionnement

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

#define PRESCALER 256
#define NB_TICK  1250
#define CTC1  WGM12  


void init_timer(void){
TCCR1A = 0;  
TCCR1B = 1<<CTC1; 
#if (PRESCALER==8)
 TCCR1B |= (1<<CS11);
#elif (PRESCALER==64)
 TCCR1B |= (1<<CS11 | 11<<CS10);
#elif (PRESCALER==256)
 TCCR1B |= (1<<CS12);
#elif (PRESCALER==1024)
 TCCR1B |= (1<<CS12 | 1<<CS10);
#endif
 
OCR1A = NB_TICK;
TCNT1 = 0;
TIMSK1 = (1<<OCIE1A);
}

ISR(TIMER1_COMPA_vect, ISR_NAKED){    // Procédure d'interruption
  PORTB ^= (1<<PB5);
}

int main(void)
{
  DDRB |= (1<<PB5);
  init_timer();
  sei();
  while(1);
}

Pour passer d'une ISR non nue à une ISR naked il faut écrire les macros de sauvegardes et de restaurations des registres, ces macros sont écrites dans des fichiers présents dans notre GIT :

  • pico_yahiani_zongo/Software/codes_Shield/ordo_tourni_2taches/save_registers.h
  • pico_yahiani_zongo/Software/codes_Shield/ordo_tourni_2taches/restore_registers.h

Dans la suite nous avons défini nos processus comme des structures que nous avons stockées dans un tableau.

typedef struct {
  void (*tache)(void);	
  int pile;
  int etat;//wake ou sleepy (processus actif ou en attente)
}tache; 
tache taches[TACHE_MAX];

Notre prochain objectif est de lancer deux tâches en parallèle en incluant un ordonnanceur à torniquet dans l'ISR. Pour cela, nous utilisons, en plus du timer présenté plus haut, une fonction d'initialisation des tâches (init_taches()) et une ISR nue, cette fois, qui gère la succession des tâches. Nous utilisons cette fois une ISR NAKED pour avoir un contrôle total sur la gestion des registres et pour éviter la surcharge des sauvegardes et/ou restaurations automatiques générées par le compilateur.

Code C Vidéo de fonctionnement

void init_taches()
{
    for(uint8_t cpt=1; cpt<TACHE_MAX; cpt++)
    {
        uint16_t sauv = SP;
        SP = taches[cpt].pile;
        uint16_t addresse=(uint16_t) taches[cpt].tache;
        asm volatile("push %0" : : "r" (addresse & 0x00ff));
        asm volatile("push %0" : : "r" ((addresse & 0xff00)>>8));
        SAVE_REGISTERS();
        taches[cpt].pile = SP;
        SP = sauv;
    }
 }

ISR(TIMER1_COMPA_vect,ISR_NAKED)
{
    // Sauvegarde du contexte de la tâche interrompue
	SAVE_REGISTERS();
  	taches[tache_courante].pile = SP; 
	tache_courante ++;
    if (tache_courante >= TACHE_MAX)
    {
      tache_courante = 0;
    }
	// Récupération du contexte de la tâche ré-activée 
	SP = taches[tache_courante].pile;
  	RESTORE_REGISTERS();
	
    asm volatile ( "reti" );
    TCNT1=0;
}

Les 2 LEDS clignotent à des fréquences differentes

Gestion de l'état endormi

Notre ordonnanceur est désormais capable de lancer plusieurs tâches en parallèle, nous pouvons donc passer à la gestion de l'état endormi de nos processus. Jusqu'à présent, pour effectuer des pauses dans l'exécution de nos tâches, nous utilisions la fonction _delay_ms en incluant la bibliothèque <util/delay.h>. Pour améliorer notre ordonnanceur, nous effectuons maintenant les pauses avec une fonction que nous avons écrite (_delay).

Pour l'utilisation de notre fonction, nous avons redéfini la structure des tâches à laquelle nous avons ajouté un champ "temps" représentant le temps pendant lequel la tâche reste endormie. Nous avons également écrit une fonction ordonnanceur qui décrémente le champ "temps" d'une tâche puis la réveille une fois le temps écoulé. Ci-dessous, vous pouvez voir à quoi ressemblent ces fonctions.

Code fonction ordonnanceur Code fonction _delay

void ordonnanceur()
{
  int cpt_ord = 0;
  uint8_t tache_suivante = tache_courante;
  for(uint8_t cpt=1; cpt<TACHE_MAX; cpt++)
   {
    if (taches[cpt].etat == SLEEPY)
    {
            taches[cpt].temps--;

            if(taches[cpt].temps <= 0)
            {

                taches[cpt].etat = WAKE;
            }

            ++cpt_ord;
    }
   }
        if (cpt_ord == (TACHE_MAX - 1))
        {
          tache_suivante = 0;
        }
        else
        {
          do
          {
              tache_suivante = (tache_suivante + 1) % TACHE_MAX;
          }  while (taches[tache_suivante].etat != WAKE);
        }
  tache_courante = tache_suivante;
}

void _delay(uint16_t temps){
cli();
taches[tache_courante].temps = temps;//
taches[tache_courante].etat = SLEEPY;
sei();
TIMER1_COMPA_vect();
}

La nouvelle structure de tâche se presente de la façon suivante :

Code de la structure finale representant les tâches
typedef struct {
  void (*tache)(void);
  uint16_t pile;
  uint16_t temps;
  uint16_t etat;
}tache

Ci-dessous, une vidéo de fonctionnement de la gestion de l'état endormi des processus. Vous pouvez voir au début de la vidéo que seule la LED verte est active (état WAKE) pendant que la LED orange est endormie. Une fois le temps d'endormissement écoulé, vous pouvez voir la LED orange commencer à clignoter (elle est passée à l'état WAKE) à la même fréquence que la LED verte.

Pour une bonne gestion de l'état endormi, nous ajoutons à notre liste de tâches une tâche _wait qui ne fait rien en bouclant juste indéfiniment. Cette dernière est importante car lorsque toutes les autres tâches de l'ordonnanceur sont inactives ou en attente, une tâche "neutre" garantit qu'il y a toujours une activité en cours, même si elle est minimale. Cela évite que le processeur tombe dans un état imprévisible ou commence à exécuter du code non souhaité.

Liste des tâches Vidéo de fonctionnement

tache taches[TACHE_MAX] =
{{_wait,0x0800,10,WAKE},
{gestion_LED4,0x0400,1000,SLEEPY},
{gestion_LED2,0x0600,10,WAKE},
};

Gestion communication Port serie et SPI

Avec afficheur sept-segments

Notre ordonnanceur est maintenant capable de gérer le clignotement de deux LEDs en parallèle ainsi que les processus endormis. Il s'agira maintenant pour nous de le complexifier en lui permettant de gérer les communications SPI et port série. Pour ce faire, nous utilisons des codes grandement inspirés de ceux présents sur le site de M. REDON (https://rex.plil.fr/Enseignement/Systeme/Systeme.PSE/systeme.html), qui se trouvent dans les fichiers SPI.c (pour la gestion de la communication SPI) et serial.c (pour le port série).

Gestion SPI Gestion port série

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

#define SPI_DDR         DDRB
#define SS_DDR          DDRC
#define SPI_PORT        PORTC


#define SPI_SS_M        2 
#define SPI_MOSI        3
#define SPI_MISO        4
#define SPI_SCK         5




void spi_activer(void){                              // Activer le périphérique
SPI_PORT &= ~(1<<0);                            // Ligne SS à l'état bas
}

void spi_desactiver(void){                           // Désactiver le périphérique
SPI_PORT |= (1<<0);                             // 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 spi_init(void){                                 // Initialisation du bus SPI
SPI_DDR |= (1<<SPI_MOSI)|(1<<SPI_SCK)|(1<<SPI_SS_M);   // Définition des sorties
SPI_DDR &= ~(1<<SPI_MISO);                           // Définition de l'entrée
                             // Désactivation du périphérique
SS_DDR |= (1<<0); // afficheur sur pc0
SPCR = (1<<SPE)|(1<<MSTR)|(1<<SPR1);                 // Activation SPI (SPE) en état maître (MSTR)
SPI_PORT |= (1<<0);                                                   // horloge F_CPU/64 (SPR1=1,SPR0=0)
}

#include <avr/io.h>

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

char valeur;

void serie_init(long int vitesse){
UBRR0=F_CPU/(((unsigned long int)vitesse)<<4)-1; // configure la vitesse
UCSR0B=(1<<TXEN0 | 1<<RXEN0);                    // autorise l'envoi et la réception
UCSR0C=(1<<UCSZ01 | 1<<UCSZ00);                  // 8 bits et 1 bit de stop
UCSR0A &= ~(1 << U2X0);                          // double vitesse désactivée
}

void serie_envoyer(unsigned char c){
loop_until_bit_is_set(UCSR0A,UDRE0);
UDR0=c;// charge l'octet à envoyer dans le registre de transmission
}

unsigned char serie_recevoir(void){
loop_until_bit_is_set(UCSR0A, RXC0);
return UDR0; //retourne le caractère reçu 
}

void set_valeur(void){
    while(1){
        valeur = serie_recevoir();
         serie_envoyer(valeur);
    }
}

Les fonctions serie_envoyer et serie_recevoir comportent chacune une phase d'attente de disponibilité avec les lignes loop_until_bit_is_set(UCSR0A, UDRE0); (qui attend que le bit UDRE0 dans le registre UCSR0A soit à 1 ; ce bit indique que le registre de transmission (UDR0) est prêt à recevoir un nouvel octet) et loop_until_bit_is_set(UCSR0A, RXC0); (qui attend que le bit RXC0 dans le registre UCSR0A soit à 1 ; ce bit indique qu'un octet a été reçu via le port série et est disponible dans le registre UDR0).

Il y a ensuite une phase de transmission pour serie_envoyer avec UDR0 = c; : l'octet est chargé dans le registre UDR0. Une fois chargé, l'octet est automatiquement envoyé via le port série. Pour serie_recevoir, la phase suivante est la lecture avec return UDR0; la fonction renvoie directement le contenu du registre UDR0, qui contient l'octet reçu.

Dans le fichier serial.c (image droite ci-dessus), nous avons ajouté la fonction set_valeur qui utilise simultanément les fonctions serie_envoyer et serie_recevoir pour gérer l'écriture et la lecture via le port série dans la même tâche. La valeur mise dans serie_recevoir est la même que celle utilisée dans la tâche de gestion de l'afficheur à sept segments. Nous ajoutons ensuite set_valeur dans notre liste de tâches. Vous pouvez voir le résultat que nous obtenons ci-dessous.

Liste des tâches Vidéo de fonctionnement

tache taches[TACHE_MAX] =
{{_wait,0x0800,10,SLEEPY},
{gestion_LED4,0x0400,1000,SLEEPY},
{gestion_LED2,0x0600,10,WAKE},
{ctrl7segments,0x500,10,WAKE},
{set_valeur,0x300,10,WAKE}
};

Avec Matrice de LEDS

De la même manière qu'avec le sept-segments, nous utilisons notre ordonnanceur pour commander la matrice de LEDs par l'intermédiaire des modes de communication SPI et port série. À la différence du sept-segment, la matrice de LEDs nécessite une configuration des différents caractères qu'elle pourra afficher. Pour cela, nous initialisons une matrice définissant les LEDs qui doivent s'allumer pour afficher les caractères souhaités. Nous affichons l'ensemble des caractères hexadécimaux grâce à cette matrice (contenue dans le fichier matrice.h). Ci-dessous, vous pouvez voir les résultats que nous obtenons ainsi que les codes que nous avons utilisés.

Matrice de configuration des LEDs

char hex[16][8] = {
    {0x7e,0x42,0x42,0x42,0x42,0x42,0x7e,0x00}, //--> 0
    {0x08,0x18,0x28,0x08,0x08,0x08,0x3e,0x00}, //--> 1
    {0x18,0x24,0x04,0x08,0x10,0x20,0x7c,0x00}, //--> 2
    {0x18,0x24,0x04,0x18,0x04,0x04,0x38,0x00}, //--> 3
    {0x20,0x20,0x24,0x24,0x3e,0x04,0x04,0x00}, //--> 4
    {0x7e,0x40,0x40,0x7e,0x02,0x02,0x7e,0x00}, //--> 5
    {0x7e,0x40,0x40,0x7e,0x42,0x42,0x7e,0x00}, //--> 6
    {0x7f,0x02,0x04,0x08,0x10,0x20,0x40,0x00}, //--> 7
    {0x3c,0x42,0x42,0x3c,0x42,0x42,0x3c,0x00}, //--> 8
    {0x7e,0x42,0x42,0x7e,0x02,0x02,0x7e,0x00}, //--> 9
    {0x18,0x24,0x42,0x42,0x7e,0x42,0x42,0x42}, //--> A
    {0x7c,0x42,0x42,0x42,0x7c,0x42,0x42,0x7e}, //--> B
    {0x7e,0x40,0x40,0x40,0x40,0x40,0x40,0x7e}, //--> C
    {0x78,0x44,0x42,0x42,0x42,0x42,0x42,0x7c}, //--> D
    {0x7e,0x40,0x40,0x7e,0x40,0x40,0x40,0x7e}, //--> E
    {0x7e,0x40,0x40,0x7c,0x40,0x40,0x40,0x40}, //--> F
};

Code de gestion de la matrice de LEDs Vidéo de fonctionnement
int selection(char select)// pour choisir l index dans le tableau pour la matrice
{
    int result = 0;
    if(select >= '0' && select <= '9')
        result = select - 48;
    else if(select >= 'a' && select <= 'f')
        result = 10 + (select - 'a');
    else if(select >= 'A' && select <= 'F')
        result = 10 + (select - 'A');

    return result;
}
void aff_matrix(void){
    char a;
    cli();
    int index;
    spi_activer(); //Activate the RGB Matrix
    spi_echange('%');
    spi_echange(1);
    // spi_echange(0x26); 
    spi_desactiver(); //Activate the RGB Matrix
    while(1){
        index = selection(valeur);
        spi_activer();
        //sleepy(50);
        for(int LED=0; LED<8; LED++){
            for(int j=0; j<8;j++){
                a = hex[index][LED] & (1<<j);
                spi_echange(a);
            }
        }
        _delay(10);
        spi_desactiver();
        _delay(25);

    }
    sei();
}

Matrice de LEDs programmée

Carte fille écran LCD

Conception de la carte fille

La carte fille écran comporte un ATMega328p et un écran LCD à base de contrôleur HD44780. Nous utilisons, comme precisé dans le wiki, un potentiomètre pour régler la luminosité des cristaux liquides ainsi qu'un connecteur HE10 pour la connexion de cette carte fille à notre carte mère sur lequel nous avons bien prevu une ligne de sélection SPI. Aucune ligne d'interruption n'était initialement nécessaire, mais suite au rajout d'une RAM SPI (FM25W256-G) nous avons dû en rajouter une pour gérer les interruptions.La carte mère écrit dans la RAM et le microcontôleur de la carte fille rafraichit l'écran régulièrement; en outre pour éviter les conflits entre la carte mère qui écrit et la carte fille qui lit il est nécessaire d'effectuer une gestion de priorités entre les différents signaux de MOSI, d'horloge et de selection. Cette gestion se fait à l'aide de circuits intermédaires présents dans notre schématique inspirés des circuits "NMOS highside" que nous avons réalisé avec l'aide de nos Encadrants M. BOE et M. REDON.

Voici les étapes de conceptions de notre carte fille:

Première Version

Schématique de notre carte fille écran
Routage de notre carte fille écran
Visualisation 3D de notre carte PCB (face avant)
Visualisation 3D de notre carte PCB (face arrière)

réception de la carte PCB:

Carte fille avant soudure Carte fille après soudure
Carte.pdf.pdf
First cart soud.jpg


Problèmes rencontrés avec la première version de la carte

Lors de la conception et du routage de la première version de la carte destinée à gérer l'écran LCD HD44780, plusieurs erreurs ont été identifiées. Ces erreurs ont gravement impacté le fonctionnement de la carte, rendant nécessaire la conception d'une nouvelle version. Voici un récapitulatif des problèmes rencontrés et de leur impact :

  • Erreur sur l'empreinte du regulateur ISR

Une empreinte SMD a été utilisée pour le régulateur ISR, alors que nous ne disposions que du composant en version traversante. Nous avons dû improviser en soudant des fils pour relier les différentes broches (MOSI, MISO, RST, etc.). Cette approche temporaire a engendré des problèmes majeurs, tels que des connexions instables, des court-circuits et une exposition accrue au bruit électromagnétique. Ces problèmes ont compromis la fiabilité des communications SPI et l'alimentation du circuit, comme nous l'a fait remarquer l'un de nos encadrants, M. REDON. Nous avons donc dans la nouvelle version, remplacé cette empreinte par sa version traversante.

  • Connexion incorrecte des broches E et RS

Les broches E (Enable) et RS (Register Select) de l'écran LCD ont été connectées directement au 5V, au lieu d’être reliées aux broches programmables de l’ATMega328p. Ces broches jouent un rôle essentiel dans la communication avec le contrôleur HD44780, en permettant de sélectionner le registre de commande ou de données et de déclencher des opérations d'écriture. En les connectant directement au 5V, il était impossible de les programmer ou de les utiliser correctement. Cela a totalement empêché la configuration et l’envoi de commandes à l’écran LCD. Dans la nouvelle version de la carte, nous avons donc relié les broches E et RS à des broches numériques de l’ATMega328p pour assurer leur contrôle logiciel.

  • Erreur sur l'alimentation de l’écran LCD

L'alimentation de l’écran a été liée en série avec un condensateur, au lieu d'être connectée en parallèle. Cette configuration en série empêchait la bonne alimentation de l'écran après les modifications effectuées sur les broches RS et E, rendant ce dernier inopérant. Nous avons donc modifié le schéma pour connecter le condensateur en parallèle avec l’alimentation, ce qui est nécessaire pour stabiliser la tension d’alimentation, comme nous l'a fait remarquer l'un de nos encadrants, M. BOE.

Seconde Version

schematique carte fille 2nde version
routage carte fille 2nde version
visualisation 3D (face avant)
visualisation 3D (face arrière)

Une fois la seconde version de la carte reçue nous somme passés à la soudure des différents composants. Ci-dessous vous pouvez voir notre carte fille avant et après la phase de soudure.

Carte fille avant soudure Carte fille après soudure
Carte non soudée.jpg
Carte .jpg

Programmation de la carte fille

Test de la carte fille

Avant d'entamer la gestion des différentes fonctionnalités de l'écran LCD, nous vérifions si les composants ont été bien soudés et si la carte est fonctionnelle et programmable, en faisant clignoter une de ses LEDs avec du code Arduino présent dans notre répertoire Git. Vous pouvez voir les résultats de ce test ci-dessous :

Code arduino Vidéo fonctionnement
#include <SPI.h>
#include <avr/io.h>

#define LED_PIN 4
#define DELAY_MS 500

void setup()
{
  DDRB |= (1 << LED_PIN);
}

void loop()
{
  PORTB |= (1 << LED_PIN);
  delay(DELAY_MS);

  PORTB &= (1 << LED_PIN);
  delay(DELAY_MS);
}

LED d'alimentation allumée et LED programmable clignotante

Test de l'écran LCD HD44780

Maintenant que notre carte fille est fonctionnelle, nous commençons la programmation de notre écran LCD. Encore une fois, nous passons par une étape de test où nous vérifions le bon fonctionnement de l'écran LCD. À l'aide de code Arduino, nous affichons le classique "Hello, World!" sur notre écran. Vous pouvez voir les résultats de ce test ci-dessous.

Code arduino Image résultat
#include <LiquidCrystal.h>

// initialize the library by associating any needed LCD interface pin
// with the arduino pin number it is connected to
const int rs = 18, en = 19, d4 = 3, d5 = 4, d6 = 5, d7 = 6;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

void setup()
{
  pinMode(15, OUTPUT);
  digitalWrite(15, LOW);
  // set up the LCD's number of columns and rows:
  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.print("hello, world!");
}

void loop()
{
  // set the cursor to column 0, line 1
  // (note: line 1 is the second row, since counting begins with 0):
  lcd.setCursor(0, 1);
  // print the number of seconds since reset:
  lcd.print(millis() / 1000);
}

Hello ecr.jpg

Gestion de l'affichage

Défilement

Notre écran affiche bien les caractères souhaités, nous pouvons donc passer à la réalisation des différentes fonctions de gestion de notre écran LCD HD44780. Pour ce faire, nous avons utilisé le code de gestion du HD44780 que nous a donné l'un de nos encadrants, M. REDON. En utilisant les fonctions présentes dans ce code nous arrivons à faire défiler des caractères sur notre écran.

Code arduino Image résultat
int main(void)
{
  //Configuration et séquence d'initilisation de l'écran LCD
  HD44780_Initialize();
  HD44780_WriteCommand(LCD_ON|CURSOR_NONE);
  HD44780_WriteCommand(LCD_CLEAR);
  HD44780_WriteCommand(LCD_HOME);
  HD44780_WriteCommand(LCD_INCR_RIGHT);
  _delay_ms(50);

  //display_HB(MESSAGE);
 
  while (1) {
        display_GD(MESSAGE); // Affiche le message sur l'écran
        defiler_GD(MESSAGE);
        _delay_ms(5);      // Délai pour rendre le mouvement lisible
    }
}

Dans l'exemple ci-dessus nous utilisons les fonctions display_GD et defiler_GD que nous plaçons dans une boucle infinie ce qui nous permet de gérer un défilement continu. Vous pouvez voir le contenu de ces fonctions ci-dessous.

display_GD defiler_GD
void display_GD(char *message) {
    for(int c = 0; c < NB_COLS; c++){
        int address=HD44780_XY2Adrr(NB_ROWS,NB_COLS,0,c);
        HD44780_WriteCommand(LCD_ADDRSET|address);
        if(message[c] == '\0')
            HD44780_WriteData(' ');
        else
            HD44780_WriteData(message[c]);
    }
    if(strlen(message) > 16){
        defiler_GD(message);
        _delay_ms(50);
    }
}

void defiler_GD(char *message) {
    char temp = message[0];
    int length = strlen(message);
    for(int i = 0; i < length - 1; i++) {
        message[i] = message[i+1];
    }
    message[length-1] = temp;
}

La fonction display_GD affiche le message sur une seule ligne de l'écran et le fait défiler si sa longueur dépasse 16 caractères, tandis que defiler_GD sauvegarde le premier caractère du message puis décale tous les caractères vers la gauche et enfin replace le premier caractère à la fin du message. Le message est donc modifié pour simuler un défilement.


Caractères spéciaux et quelques codes VT100

Pour gérer les caractères spéciaux nous avons écrit la fonction process_special_characters qui, avec ses différentes conditions, prend en compte les effets attendus pour le retour à la ligne (\n), le retour chariot (\r), et quelques codes VT100 qui déplacent le curseur. L'implémentation de cette fonction est visible ci-dessous.

fonction de gestion des caractères spéciaux
void process_special_characters(char *message) {
    static int current_row = 0;
    static int current_col = 0;
    static int message_length = 0; // pour stocker la longueur du message affiché

    // Si le message a été entièrement affiché, réinitialiser les indices.
    if (message_length == strlen(message)) {
        current_row = 0;
        current_col = 0;
    }

    // On garde une trace de la longueur du message affiché pour éviter les répétitions.
    for (int i = message_length; message[i] != '\0'; i++) {
        if (message[i] == '\n') {
            // Saut de ligne : on passe à la ligne suivante
            current_row = (current_row + 1) % NB_ROWS;
            current_col = 0;  // Retour au début de la nouvelle ligne
        } else if (message[i] == '\r') {
            // Retour chariot : on revient au début de la ligne actuelle
            current_col = 0; // Retour au début de la ligne
        } else if (message[i] == '\033') { // Détection des séquences VT100
            if (message[i + 1] == '[') {
                int j = i + 2;  // Position après '\033['
                int value = 0;   // Valeur numérique du déplacement
                // Lire les chiffres jusqu'à ce qu'on atteigne un caractère non numérique
                while (message[j] >= '0' && message[j] <= '9') {
                    value = value * 10 + (message[j] - '0');
                    j++;
                }
                // Vérifier et appliquer le déplacement en fonction du caractère suivant
                if (message[j] == 'H') { // Déplacement en haut à gauche (reset du curseur)
                    current_row = 0;
                    current_col = 0;
                } else if (message[j] == 'A') { // Curseur vers le haut
                    current_row = (current_row - value + NB_ROWS) % NB_ROWS;
                } else if (message[j] == 'B') { // Curseur vers le bas
                    current_row = (current_row + value) % NB_ROWS;
                } else if (message[j] == 'C') { // Curseur vers la droite
                    current_col = (current_col + value) % NB_COLS;
                } else if (message[j] == 'D') { // Curseur vers la gauche
                    current_col = (current_col - value + NB_COLS) % NB_COLS;
                }
                i = j;  // Mettre à jour l'index pour sauter les caractères traités
            }
        } else {
            // Affichage du caractère normal
            int address = HD44780_XY2Adrr(NB_ROWS, NB_COLS, current_row, current_col);
            HD44780_WriteCommand(LCD_ADDRSET | address);
            HD44780_WriteData(message[i]);
            current_col = (current_col + 1) % NB_COLS;

            // Gestion du retour automatique à la ligne (si nécessaire)
            if (current_col == 0) {
                current_row = (current_row + 1) % NB_ROWS;
            }
        }

        // Ajout d'un petit délai pour afficher un caractère à la fois
        _delay_ms(20); // Délai de 20ms entre chaque caractère pour l'effet d'affichage
    }

    // Mettre à jour la longueur du message affiché
    message_length = strlen(message);
}

  • Retour à la ligne

Le code présenté ci-dessus nous permet de gérer le retour à la ligne. Nous affichons la chaîne de caractères "retour...\n a la ligne". Vous pouvez voir le résultat du test ci-dessous.

section de gestion du retour à la ligne Vidéo de fonctionnement
 for (int i = message_length; message[i] != '\0'; i++) {
        if (message[i] == '\n') {
            // Saut de ligne : on passe à la ligne suivante
            current_row = (current_row + 1) % NB_ROWS;
            current_col = 0;  // Retour au début de la nouvelle ligne
}

  • Retour chariot

Nous pouvons également gérer le retour chariot. Nous affichons la chaîne de caractères "retour...\r charriot". Vous pouvez voir le résultat du test ci-dessous.

section de gestion du retour à la ligne + retour chariot Vidéo de fonctionnement
 for (int i = message_length; message[i] != '\0'; i++) {
        if (message[i] == '\n') {
            // Saut de ligne : on passe à la ligne suivante
            current_row = (current_row + 1) % NB_ROWS;
            current_col = 0;  // Retour au début de la nouvelle ligne
} else if (message[i] == '\r') {
            // Retour chariot : on revient au début de la ligne actuelle
            current_col = 0; // Retour au début de la ligne
}

  • Codes VT100

Nous pouvons également gérer quelques codes VT100. Nous affichons la chaîne de caractères "PICO\033[3Cecran\033[1Bbin6\033[1B \rbye ;)". Vous pouvez voir le résultat du test ci-dessous.

section de gestion du retour à la ligne + retour chariot + VT100 Vidéo de fonctionnement
 for (int i = message_length; message[i] != '\0'; i++) {
        if (message[i] == '\n') {
            // Saut de ligne : on passe à la ligne suivante
            current_row = (current_row + 1) % NB_ROWS;
            current_col = 0;  // Retour au début de la nouvelle ligne
        } else if (message[i] == '\r') {
            // Retour chariot : on revient au début de la ligne actuelle
            current_col = 0; // Retour au début de la ligne
        } else if (message[i] == '\033') { // Détection des séquences VT100
            if (message[i + 1] == '[') {
                int j = i + 2;  // Position après '\033['
                int value = 0;   // Valeur numérique du déplacement
                // Lire les chiffres jusqu'à ce qu'on atteigne un caractère non numérique
                while (message[j] >= '0' && message[j] <= '9') {
                    value = value * 10 + (message[j] - '0');
                    j++;
                }
                // Vérifier et appliquer le déplacement en fonction du caractère suivant
                if (message[j] == 'H') { // Déplacement en haut à gauche (reset du curseur)
                    current_row = 0;
                    current_col = 0;
                } else if (message[j] == 'A') { // Curseur vers le haut
                    current_row = (current_row - value + NB_ROWS) % NB_ROWS;
                } else if (message[j] == 'B') { // Curseur vers le bas
                    current_row = (current_row + value) % NB_ROWS;
                } else if (message[j] == 'C') { // Curseur vers la droite
                    current_col = (current_col + value) % NB_COLS;
                } else if (message[j] == 'D') { // Curseur vers la gauche
                    current_col = (current_col - value + NB_COLS) % NB_COLS;
                }
                i = j;  // Mettre à jour l'index pour sauter les caractères traités
            }
        }

Test sur la RAM (FM25W256-G)

Afin de vérifier le bon fonctionnement de notre RAM (FM25W256-G), nous tentons d'écrire dessus, puis d'afficher les caractères écrits sur notre écran (HD44780). Pour cela, nous utilisons le code de gestion de l'affichage sur l'écran, auquel nous ajoutons quelques fonctions de notre conception permettant l'écriture sur la RAM.

Pour la configuration de la RAM, nous utilisons le tableau de commandes ci-dessous présent dans la datasheet du FM25W256-G.

Table de commande FM25W256-G
Table com.png

Ces commandes sont définies dans notre code

Définition des commandes de la RAM
// Instructions SPI pour la RAM FM25W256
#define WREN  0x06   // Write Enable
#define WRITE 0x02   // écriture dans la mémoire
#define READ  0x03   // lecture de la mémoire
#define FRAM_SIZE 32768  // Taille de la mémoire FM25W256 en octets (32 Ko)

Les principales fonctions que nous utilisons pour la configuration de la RAM SPI sont FRAM_read ,FRAM_write et FRAM_clear qui nous permettent respectivement de lire le contenu de la RAM, d'écrire dans la RAM et enfin d'effacer le contenu de la RAM. Vous pouvez voir ces fonctions ci-dessous.

Définition des fonctions pour la RAM FM25W256
Lecture de la RAM
void FRAM_read(uint16_t address, char *buffer, uint8_t length) {
    CS_LOW();
    SPI_transfer(READ);              // Commande de lecture
    SPI_transfer((address >> 8) & 0xFF);  // Adresse haute
    SPI_transfer(address & 0xFF);         // Adresse basse

    for (uint8_t i = 0; i < length; i++) {
        buffer[i] = SPI_transfer(0x00);  // Lire les données
    }
    
    CS_HIGH();
}
Écriture dans la RAM
void FRAM_write(uint16_t address, char *data) {
    FRAM_write_enable();

    CS_LOW();
    SPI_transfer(WRITE);           // Commande d'écriture
    SPI_transfer((address >> 8) & 0xFF);  // Adresse haute
    SPI_transfer(address & 0xFF);         // Adresse basse

    for (uint8_t i = 0; i < strlen(data); i++) {
        SPI_transfer(data[i]);      // Écrire chaque caractère
    }

    CS_HIGH();
}
Effaçage de la RAM
void FRAM_clear() {
    FRAM_write_enable();  // Activer l'écriture

    CS_LOW();
    SPI_transfer(WRITE);                // Commande d'écriture
    SPI_transfer(0x00);                  // Adresse haute (MSB)
    SPI_transfer(0x00);                  // Adresse basse (LSB)

    for (uint16_t i = 0; i < FRAM_SIZE; i++) {
        SPI_transfer(0x00);  // Écrire 0x00 à chaque adresse
    }

    CS_HIGH();
}

Grâce à ces fonctions nous écrivons la chaîne de caractères "PICO_Bin6" dans la RAM SPI puis nous affichons le contenu de la RAM avec notre écran HD44780. Vous pouvez voir le résultat ci-dessous.

Utilisation des fonctions dans le main image résultat
int main(void) {

    HD44780_Initialize();
    HD44780_WriteCommand(LCD_ON|CURSOR_NONE);
    HD44780_WriteCommand(LCD_CLEAR);
    HD44780_WriteCommand(LCD_HOME);
    HD44780_WriteCommand(LCD_INCR_RIGHT);
    _delay_ms(50);

    char read_buffer[10] = {0};  // Buffer pour lire les données

    SPI_init();
    FRAM_clear(); // Effacer toute la mémoire avant d'écrire

    // Écriture dans la RAM à l'adresse 0x0000
    FRAM_write(0x0000, "PICO_bin6");

    _delay_ms(100);

    // Lecture des données depuis l'adresse 0x0000
    FRAM_read(0x0000, read_buffer, 9);

    read_buffer[9] = '\0';  // Ajout d'un caractère de fin de chaîne

    while (1) {
        // Boucle infinie
        display_GD(read_buffer);
        _delay_ms(500);
    }

    return 0;
}
Lect ram.jpg

Nous n'arrivons cependant pas à expliquer l'origine des caractères présents après ceux que nous avons écrits sur la RAM. Notre principale hypothèse est qu'il s'agit de caractères initialement présents sur la RAM, qui n'ont pas été effacés. Nous décidons néanmoins de continuer dans notre gestion de la communication avec la carte fille.

Communication Picoshield-écran

Après avoir testé le bon fonctionnement de la RAM nous passons à la communication SPI entre le PicoShield et la carte fille écran. Le Picoshield envoie des caractères par SPI qui sont réçus puis traités via minicom par la carte fille qui les affiche ensuite sur l'écran lcd HD44780. Pour réaliser cette tâche nous programmons d'abord le picoshield en tant que maître pour la gestion de l'envoi des caractères, puis c'est au tour de la carte fille écran d'être programmée en esclave pour recevoir et afficher les caractères envoyés par le picoshield. Vous pouvez voir les codes de programmation des cartes picoshield et écran ci-dessous.

**Aperçu des Codes SPI Esclave (écran) et SPI Maître (picoshield)**
**Code SPI Esclave (Affichage LCD)** **Code SPI Maître**
#include <avr/io.h>
#include <util/delay.h>
#include "lcd.h"
#define F_CPU 16000000UL

void spi_init_slave(void) {
    DDRB &= ~((1 << PB3) | (1 << PB5));  // MOSI et SCK en entrée
    DDRB |= (1 << PB4);                  // MISO en sortie
    SPCR = (1 << SPE);                   // Activer le SPI en mode esclave
}

char spi_recevoir_caractere(void) {
    while (!(SPSR & (1 << SPIF)));  // Attendre la réception complète
    return SPDR;                   // Retourner la donnée reçue
}

void afficher_caractere_sur_LCD(char caractere) {
    HD44780_WriteData(caractere);
}

int main(void) {
    // Initialisation SPI et LCD
    spi_init_slave();
    HD44780_Initialize();
    HD44780_WriteCommand(LCD_ON | CURSOR_BLINK);
    HD44780_WriteCommand(LCD_CLEAR);
    HD44780_WriteCommand(LCD_HOME);

    while (1) {
        // Réception d'un caractère via SPI
        char caractere = spi_recevoir_caractere();

        // Affichage du caractère reçu sur l'écran LCD
        afficher_caractere_sur_LCD(caractere);
    }

    return 0;
}
#include <avr/io.h>
#include <avr/interrupt.h>
#include "SPI.h"
#include "serial.h"

#define F_CPU 16000000UL  
#include <util/delay.h>

// Définition des broches SPI
#define SPI_SS_PORT PORTC
#define SPI_SS_PIN  PC0

char valeur;

void spi_envoyer_caractere(char c) {
    spi_activer();
    spi_echange(c);
    spi_desactiver();
}

void set_valeur(void) {
    while (1) {
        valeur = serie_recevoir();  // Lecture du caractère depuis Minicom
        serie_envoyer(valeur);      // Echo vers le terminal Minicom
        spi_envoyer_caractere(valeur);  // Transmission du caractère via SPI
    }
}

int main(void) {
    spi_init();        // Initialisation du SPI
    serie_init(9600);  // Initialisation du port série

    sei();  // Activation des interruptions

    while (1) {
        set_valeur();
    }

    return 0;
}

Une fois les cartes programmées nous connectons notre écran à notre Picoshield par le port HE-10 de ce dernier. Nous ouvrons ensuite un terminal minicom avec la commande suivante "minicom -D /dev/ttyUSB0 -b 9600" et envoyons les caractères qui s'affichent sur l'écran. Vous pouvez voir cela dans la vidéo ci-dessous.

**Vidéo Démonstration**

Bilan & Conclusion

Nous sommes au terme de notre projet, et les objectifs que nous avons atteints sont les suivants :

  • Une carte Shield fonctionnelle et programmable.
  • Un ordonnanceur fonctionnel avec différentes tâches (LEDs clignotantes, affichage sept-segments et matrice de LEDs), fonctionnant simultanément et disposant chacune des états "WAKE" et "SLEEPY".
  • Une carte fille écran LCD fonctionnelle, avec un écran gérant l'affichage des caractères spéciaux ainsi que quelques commandes VT100 et une mémoire RAM SPI sur laquelle nous pouvons lire et écrire.

Cependant, faute de temps, nous n'avons pas pu aborder les primitives systèmes. En effet, nous avons pris beaucoup plus de temps que prévu pour concevoir une carte fille fonctionnelle (la première carte présentait de nombreux problèmes), un temps précieux qui nous aurait sans doute permis de réaliser les primitives systèmes manquantes.

Ce projet, qui s'est étendu sur tout le semestre, nous a permis d'approfondir nos connaissances en conception de cartes électroniques, en analyse de codes C, ainsi que notre compréhension des microcontrôleurs en général.

Nous tenons à remercier nos encadrants, M. REDON et M. BOE, pour leur accompagnement et les conseils donnés tout au long du projet.