SE4Binome2025-5

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

Objectif

L'objectif de notre groupe est de réaliser une carte fille compatible avec la carte mère réalisée par le binôme 2. Cette carte que nous réalisons doit pouvoir permettre au pico-ordinateur d'afficher des informations sur un écran. Pour complexifier la tâche, nous utiliserons un affichage LCD graphique, qui nous permettra d'effectuer un affichage simple initialement, mais aussi de créer des primitives graphiques dans un second temps.

En plus de cette carte, nous devons réaliser un "OS primitif" qui fonctionnera sur une carte Arduino, à la fois pour pouvoir comprendre le fonctionnement d'un ordonnanceur, mais aussi pour pouvoir tester notre carte de notre côté avant de l'intégrer au pico-ordinateur. L'Arduino sera complété d'un shield afin d'avoir une interface similaire à celle qui sera disponible sur la carte mère, que nous devons réaliser également. Aussi, ce shield sera accompagné d'un module de carte SD pour pouvoir charger des programmes "à la volée" depuis la carte SD.

Dépôt GIT

le dépôt git de ce projet est le suivant: https://gitea.plil.fr/avanghel/SE4-Pico-B5

Partie Hardware

Bouclier Arduino & carte SD

Le bouclier Arduino que nous réalisons servira d'interface entre l'Arduino et la carte fille, de façon à ce qu'un arduino avec le programme de la carte mère et le shield d'installé ait le même comportement que la carte mère. Le module SD pourra se brancher sur un connecteur de la carte mère et donc du bouclier aussi.

Composants utilisés

Composants du bouclier arduino

La liste des composants utilisés pour le bouclier est la suivante:

-5x LEDS rouges

-5x Résistances 1kohm

Connecteurs utilisés (tous en 2.54mm):

-Connecteur arduino R3(disponible dans la librairie d'empreintes KiCad)

-6x 1x8 mâle vertical

Composants du module SD

La liste des circuits intégrés utilisés pour le module SD est la suivante:

-1x LTC3531

-1x74LVC125

les composants hors circuits intégrés utilisés sont les suivants:

-1x Capacité 2.2µF

-1x Capacité 10µF

-1x Inductance 10µH

-3x Résistances 3.3kohms

Les connecteurs utilisés sont les suivants:

-1x 1x8 mâle horizontal

-1x Connecteur Molex microSD

Schématique des deux cartes

Ci dessous la schématique pour les deux cartes:

Schématique du bouclier arduino et du module SD
Schématique du bouclier arduino et du module SD

PCB du bouclier

Ci dessous le PCB du bouclier arduino:

PCB de la carte bouclier arduino.
PCB de la carte bouclier arduino.

PCB du module SD

Ci dessous le PCB du module SD. Il est à noter que le connecteur SD est à l'envers, et que l'insertion d'une carte SD est difficile de ce fait. Il est recommandé de changer le routage du module SD si le projet est réutilisé.

PCB du module SD
PCB du module SD

Vue 3D des deux cartes

Ci-dessous la vue 3D de la carte du bouclier et du module SD:

Vue 3D du bouclier arduino et du module SD
Vue 3D du bouclier arduino et du module SD

Carte Fille

Le pilotage d'un écran graphique est plus complexe que pour un écran à caractère, puisque la création d'une image se fait pixel par pixel (plutôt que par caractère pour l'écran à caractère). De plus, le microcontrôleur de la carte mère(que l'on appelera CPU ici) doit gérer d'autres cartes filles, il n'est donc pas souhaitable qu'il traite par lui-même une tâche aussi lourde.

Notre carte fille devra donc être composée elle aussi d'un microcontrôleur(que l'on appelera GPU) qui interprètera des données reçues par le CPU (par exemple, un caractère à une certaine position) et transmettra les données à afficher à l'écran LCD graphique. Par ailleurs, nous fixerons l'écran graphique sur la carte fille.

Composants utilisés

La liste des circuits intégrés est la suivante:

-Atmega328P(Microcontrôleur)

-FM256WG (mémoire SPI)

-TMUX1574 (Multiplexeur bidirectionnel 4 bits) (à commander)

La liste des composants hors CI est la suivante:

-1 oscillateur à quartz 8MHz

-2 Capacités de 22pF

-5 Capacités de 100nF

-1 Capacité de 10µF

-1 Résistance de 1 Mohm

-2 Résistances de 1 Kohm

-2 LEDs rouges

Les connecteurs sont tous des connecteurs traversants 2.54 mm, les voici:

- 2x3 mâle vertical (ISP)

- 1x2 mâle vertical (debug, connection série)

- 1x8 mâle horizontal (Interface mère-fille)

- 1x9 mâle vertical (Interface avec écran)

Schématique de la carte fille

Schématique de la carte fille
Schématique de la carte fille

PCB de la carte

Ci-dessous le PCB de la carte imprimée:

PCB de la carte fille
PCB de la carte fille

et ci dessous un zoom sur la partie logique:

zoom sur la partie logique de la carte fille
zoom sur la partie logique de la carte fille

Vue 3D de la carte

Ci-dessous une vue 3D de la carte fille:

Vue 3D du pcb de la carte fille
Vue 3D du pcb de la carte fille

A noter que le connecteur en bas de la carte(connecteur mère-fille) sera bien à l'horizontale.

Composants à commander

TMUX1574DYYR (SOT-23-16 THIN)

Software

Ordonnanceur

Initialisation de la pile

Interruption nue

Structure d'un processus

Code de l'ordonnanceur

Fonction wait

Exemple de tâches

Exemple d'utilisation

Carte SD

Ecran LCD

Pour notre carte fille, nous utiliserons l'écran Fermion: 3.5” 480x320 TFT LCD Capacitive. Voyons le code pour communiquer avec l'écran.

Initialisation

Tout d'abord, la carte utilisant le driver ILI9488 pour communiquer avec l'écran, nous devons initialiser la communication.

Il faut savoir que ce driver reçoit soit des commandes, soit des données, et donc nous allons souvent utiliser ces 2 fonctions :

void tft_command(uint8_t cmd) {
    active_o(TFT_DC, LOW, 'd');   // DC low = command
    enable_spi();
    spi_echange(cmd);
    disable_spi();
}

void tft_data(uint8_t data) {
    active_o(TFT_DC, HIGH, 'd');   // DC high = data
    enable_spi();
    spi_echange(data);
    disable_spi();
}

Maintenant que nous avons ces fonctions, nous pouvons initialiser notre écran.

void tft_init() {
    // RESET
    tft_reset();

    // software reset
    tft_command(0x01);
    _delay_ms(100);

    ... //Suite de commandes utilisant tft_command et tft_data

    active_o(CS, HIGH, 'b');
    _delay_ms(100);
}

Par ailleurs, il faut initialiser notre SPI pour l'activer en mode maître et choisir la vitesse voulue, dans notre cas 8MHz.

void spi_init(void){
    SPCR = (1 << SPE) | (1 << MSTR);    //Active le SPI en mode Maitre
    SPSR = (1 << SPI2X);                // SPI2X = 1 → diviseur final = 2
}

Afficher un rectangle de couleur

Pour afficher un rectangle de couleur, il faut premièrement saisir les coordonnées concernées.

void tft_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
    //tft_command(0x2C); // Memory Write
    tft_command(0x2A); // Column Address Set
    tft_data(x0 >> 8);
    tft_data(x0 & 0xFF);
    tft_data(x1 >> 8);
    tft_data(x1 & 0xFF);

    tft_command(0x00);
    tft_command(0x2B); // Page Address Set
    tft_data(y0 >> 8);
    tft_data(y0 & 0xFF);
    tft_data(y1 >> 8);
    tft_data(y1 & 0xFF);
    tft_command(0x00);
    tft_command(0x2C); // Memory Write
}

Ensuite, il faut parcourir chaque coordonnée pour venir lui associer une couleur.

void tft_fill_rect(uint16_t color, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
    tft_set_window(x0, y0, x1, y1);

    uint8_t r = ((color >> 11) & 0x1F) << 3;
    uint8_t g = ((color >> 5) & 0x3F) << 2;
    uint8_t b = (color & 0x1F) << 3;

    active_o(TFT_DC, HIGH, 'd');
    enable_spi();
    for (uint32_t i = 0; i < (uint32_t)(x1 - x0 + 1) * (uint32_t)(y1 - y0 + 1); i++) {
        spi_echange(r);
        spi_echange(g);
        spi_echange(b);
    }
    disable_spi();
}

Nous avons désormais un rectangle qui peut s'afficher de toutes les couleurs et de toutes les dimensions !

Afficher des caractères

L'affichage de caractères est un enjeu crucial dans notre projet de PicoOrdinateur car il permet de voir les commandes saisies par un utilisateur. Cependant, on ne peut pas écrire de caractère si facilement que ça, c'est pour ça que nous allons devoir utiliser une Bitmap qui nous fournit comment écrire une lettre en 5x7 pixels. Voici le code bitmap que nous avons pris de GitHub.

struct Font {
    unsigned char letter;
    unsigned char code[7][6];  // 5 caractères + '\0'
};

static const struct Font font[] = {
    { ' ', {
        "     ",
        "     ",
        "     ",
        "     ",
        "     ",
        "     ",
        "     " }},
    { 'A', {
        " ### ",
        "#   #",
        "#   #",
        "#   #",
        "#####",
        "#   #",
        "#   #" }},
    { 'B', {
        "#### ",
        "#   #",
        "#   #",
        "#### ",
        "#   #",
        "#   #",
        "#### " }},
......

Le code ci-dessus est pratique car visuel mais peu optimisé d'un point de vue mémoire car chaque information est codé par un unsigned char alors même qu'il contient seulement une information binaire. Nous pouvons alors faire 2 changements majeurs. 1. Stocker ceci dans la flash et non dans la RAM grâce à PROGMEM. 2. Représenter chaque ligne via un uint8_t. Voici la nouvelle structure utilisée.

struct Font {
   unsigned char letter;
   uint8_t rows[7];   // chaque bit = un pixel (#=1, espace=0)
};
const struct Font font[] PROGMEM = {

    { ' ', { 0x00,0x00,0x00,0x00,0x00,0x00,0x00 } },

    { 'A', { 0x0E,0x11,0x11,0x11,0x1F,0x11,0x11 } },
    { 'B', { 0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E } },
    { 'C', { 0x0E,0x11,0x10,0x10,0x10,0x11,0x0E } },
    { 'D', { 0x1E,0x11,0x11,0x11,0x11,0x11,0x1E } },
...

Nous pouvons voir que désormais, le stockage des lettres est bien moins lourd. Prenons le cas de A : 0x0E équivaut à 01110, ce qui est bien la représentation souhaitée pour la première ligne d'affichage du A.

01110
10001
10001
10001
11111
10001
10001

On retrouve bien la forme du A de la librairie d'avant, mais en utilisant beaucoup moins de place ! Pour passer de cette bitmap à l'affichage, voilà la fonction que nous utilisons :

int auto_print_char_on_screen(uint16_t color, unsigned char c, uint16_t scale)
{
    // Saut de ligne automatique
    if ((offset_x + CHAR_ESPACEMENT * scale) >= (AUTO_WINDOW_WIDTH - AUTO_BORDER_SPACE - CHAR_WIDTH * scale - 1)) {
        clear_page(scale);
        offset_y = (offset_y + LINE_ESPACEMENT * scale) % (AUTO_WINDOW_HEIGHT - AUTO_BORDER_SPACE - CHAR_HEIGHT * scale - 1);
    }

    offset_x = (offset_x + CHAR_ESPACEMENT * scale) % (AUTO_WINDOW_WIDTH - AUTO_BORDER_SPACE - CHAR_WIDTH * scale - 1);
    if (c == '\n' || c == '\r' || c == 13) {
        offset_x = 0;
        clear_page(scale);
        offset_y = (offset_y + LINE_ESPACEMENT * scale) % (AUTO_WINDOW_HEIGHT - AUTO_BORDER_SPACE - CHAR_HEIGHT * scale - 1);
        return 0;
    }

    const uint8_t* rows = get_font_bitmap(c);
    if (rows == NULL) {
        return 1;   // caractère non trouvé
    }
    // Dessin du caractère 5×7 avec scaling
    for (uint8_t py = 0; py < 7 * scale; py++) {
        uint8_t row_bits = pgm_read_byte(&rows[py / scale]);
        for (uint8_t px = 0; px < 5 * scale; px++) {
            uint8_t bit = (row_bits >> (4 - (px / scale))) & 1;

            uint16_t draw_x = AUTO_WINDOW_X + AUTO_BORDER_SPACE + offset_x + px;
            uint16_t draw_y = AUTO_WINDOW_Y + AUTO_BORDER_SPACE + offset_y + py;

            if (bit) {
                tft_draw_pixel(draw_x, draw_y, color);
            } else {
                tft_draw_pixel(draw_x, draw_y, BACKGROUND);
            }
        }
    }
    return 0;
}

La première partie permet de gérer automatiquement les sauts de ligne par rapport aux \n reçus et à la taille de l'écran. Ainsi, en recevant un flux de caractères, tout le texte se dispose de manière cohérente. Ensuite, on recherche l'index associé au caractère que l'on veut écrire. Par la suite, on parcourt chaque pixel du caractère de dimension 5*7 pour venir appliquer la couleur de la lettre ou la couleur du fond. Par ailleurs, on multiplie les dimensions par un facteur d'agrandissement pour pouvoir changer la taille des caractères.

Nous pouvons maintenant afficher des caractères, mais qu'en est-il des chaînes des caractères ?

Afficher une chaîne de caractères

Nous avons une autre fonction, sans le préfixe auto, qui permet d'afficher un texte où on veut à l'écran, pratique notamment pour écrire des élément UI en dehors de la boîte de texte. C'est par exemple le cas du texte "TERMINAL".

int print_char_on_screen(uint16_t color, unsigned char c, uint16_t x, uint16_t y, uint16_t scale);
int print_string_on_screen(uint16_t color, unsigned char *str, uint16_t x, uint16_t y, uint16_t scale);

Nous avons donc tout ce qu'il nous faut pour afficher une chaîne de caractères !

Démonstration

Exemple basique écriture via UART

Nous voyons dans cette vidéo, la démonstration de l'ensemble des fonctions vues ci-dessus. Dans le cas de ce montage, le TX de la carte mère est relié au RX de la carte écran, permettant de récupérer les données facilement, permettant d'éviter la gestion des bus SPI, qui aurait dû être initialement géré par le multiplexeur bidirectionnel 4 bits TMUX1574.

Remarque : L'affichage du saut de ligne n'est pas le même entre minicom et notre écran, tout simplement car minicom interprète la touche Entrée comme \r, peu pratique dans le cas de notre écran, nous avons donc une règle qui transforme les \r en \n pour que les lignes soient correctement sautés sur notre écran !






Exemple via communication UART puis SPI

Voici un exemple fonctionnel d'une communication en 2 étapes. Premièrement, la carte mère envoie des informations en SPI à la carte fille. Puis, une fois la touche Entrée pressé, la carte fille passe du mode SPI Slave à un mode SPI Master. Celle-ci initialise alors l'écran et affiche à l'écran les caractère précédemment écrits. On ne peut pas aller plus loin dans cette configuration à cause du bus SPI qu'on ne peut pas interchanger.






Affichage comme tâche de l'ordonnanceur

Comme nous avions un ordonnanceur fonctionnel, nous voulions vraiment envoyer les caractères à la carte fille, et ce, en passant par une tâche de l'ordonnanceur. En voici un exemple fonctionnel.