« I2L 2024 Groupe4 » : différence entre les versions

De wiki-se.plil.fr
Aller à la navigation Aller à la recherche
 
(7 versions intermédiaires par le même utilisateur non affichées)
Ligne 71 : Ligne 71 :
=== Carte au 27/02/2025 ===
=== Carte au 27/02/2025 ===


[[File:ISL-2024-G4-Realisation-B.jpg|thumb|center|400px|Réalisation terminée]]Carte finalisée avec l'écran LCD et prête à l'emploi.  
[[File:ISL-2024-G4-Realisation-B.jpg|thumb|center|400px|Réalisation terminée]]Carte finalisée avec l'écran LCD et prête à l'emploi.


= Explication du fonctionnement du code morse =
Pour comprendre la suite, il va nous falloir détailler comment fonctionne le morse, car la mesure du temps est primordiale pour comprendre comment fonctionnent la traduction et le fonctionnement.
Chaque lettre en morse correspond à une suite de points et de traits bien défini. Par exemple, le signe SOS s'écrirait <code>... --- ...</code> en morse, puisque le S est composé de 3 points et le O de trois traits, comme inscrit dans le [https://fr.wikipedia.org/wiki/Code_Morse_international code international du morse].
Un point représente une unité de temps et un trait représente trois unités de temps. Chaque point ou trait est séparé d'une unité de temps. Chaque lettre est séparé de trois unités de temps soit l'équivalent d'un trait, et un mot est séparé d'un autre par sept unités de temps.
Cela signifie donc qu'un mot comme SOS prendrait 27 unités de temps + 7 autres unités de temps si un autre mot est émis ensuite. Pour arriver à ce résultat, il suffit d'aditionner chaque caractères :
* 6 points = 6 unités de temps
* 3 traits = 3 x 3 = 9 unités de temps
* 6 unités de temps entre chaque caractère (sauf entre chaque lettre)
* et 2 x 3 unités de temps séparant les trois lettres
Il nous fallait définir des constantes précises pour l'unité de temps, puis ensuite les calcul se baserai sur le temps défini, sachant que nous ne sommes pas des experts en morse, l'idéal est de mettre des temps élevés pour qu'on arrive à formuler des messages mais à l'époque il fallait aller vite puisque le but était d'écrire vite... Le véritable porblème réside donc dans cette mesure du temps. Nous avons dû utiliser le TIMER3 de notre carte afin d'avoir un compteur de temps qui s'incrémente régulièrement afin de pouvoir facilement détecter les caractères que souhaite faire l'utilisateur.<syntaxhighlight lang="c" line="1">
/**
* @brief Initializes the hardware timer for periodic interrupts.
*
* @param diviseur Clock divider value.
* @param periode Timer period in milliseconds.
*/
void initiliaze_timer(int diviseur, long periode)
{
    CLKSEL0 = 0b00010101; // sélection de l'horloge externe
    CLKSEL1 = 0b00001111; // minimum de 8Mhz
    CLKPR = 0b10000000;  // modification du diviseur d'horloge (CLKPCE=1)
    CLKPR = 0;            // 0 pour pas de diviseur (diviseur de 1)
    TCCR3A = 0;
    TCCR3B |= (1 << WGM32); // Le mode choisi n'utilise pas ce registre
    switch (diviseur)
    {
    case 8:
        TCCR3B |= (1 << CS31);
        break;
    case 64:
        TCCR3B |= (1 << CS31 | 11 << CS30);
        break;
    case 256:
        TCCR3B |= (1 << CS32);
        break;
    case 1024:
        TCCR3B |= (1 << CS32 | 1 << CS30);
        break;
    }
    // Un cycle prend 1/F_CPU secondes.
    // Un pas de compteur prend diviseur/F_CPU secondes.
    // Pour une periode en millisecondes, il faut (periode/1000)/(diviseur/F_CPU) pas
    // soit (periode*F_CPU)/(1000*diviseur)
    OCR3A = F_CPU / PERIOD * periode / diviseur; // Calcul du pas
    TCNT3 = 0;                                  // Compteur initialisé
    TIMSK3 = (1 << OCIE3A);                      // Comparaison du compteur avec OCR1A
}
unsigned long time_counter = 0; ///< Global time counter incremented by timer interrupts
/**
* @brief Timer interrupt service routine.
*
* Increments the global time counter.
*/
ISR(TIMER3_COMPA_vect)
{
    time_counter++;
}
</syntaxhighlight>
Enfin, il est bon de noter que puisque nous nous servions d'un timer pour mesurer le temps et d'un système de communication avec le port série, l'utilisation de delais dans le code était prohibé puisque nous ne devions pas ralentir l'execution du code et uniquement nous reposer sur cette variable compteur.
Avec ces informations en notre possession, nous étions capable d'avoir une mesure du temps précise qui nous permetterai de savoir si l'utilisateur cherche à faire un son court (appui court sur le bouton) ou un son long (appui long). Ces données sont également cruciales lors de l'émission d'un message morse par la carte, puisque celle-ci doit émettre un ensemble de sons longs et courts en préservant les bonnes durées d'espacement entre les lettres et les mots afin que le mot soit audible et compréhensible.
Nous pouvions nous atteler à l'implémentation de notre projet.
= Code =
= Code =
Tout d'abord, pour développer notre projet, il nous fallait un moyen de représenter le code morse, et donc des structures :<syntaxhighlight lang="c" line="1">
Pour développer notre t, il nous fallait un moyen de représenter le code morse, et donc des structures :<syntaxhighlight lang="c" line="1">
typedef struct
typedef struct
{
{
Ligne 86 : Ligne 159 :
     char character;
     char character;
} MorseToCharEntry;
} MorseToCharEntry;
</syntaxhighlight>Nous avons également définis les valeurs associées du code morse international :<syntaxhighlight lang="c" line="1">
</syntaxhighlight>Nous avons également définis les valeurs associées du code morse international afin de pouvoir faire la correspondance entre le français et le code morse associé et inversement (puisque chaque lettre a un code qui lui est propre) :<syntaxhighlight lang="c" line="1">
CharToMorseEntry char_to_morse_table[] = {
CharToMorseEntry char_to_morse_table[] = {
     {'A', ".-"}, {'B', "-..."}, {'C', "-.-."} /* autres....*/ }};
     {'A', ".-"}, {'B', "-..."}, {'C', "-.-."} /* autres....*/ }};
Ligne 92 : Ligne 165 :
MorseToCharEntry morse_to_char_table[] = {
MorseToCharEntry morse_to_char_table[] = {
     {".-", 'A'}, {"-...", 'B'}, {"-.-.", 'C'} /* autres....*/};
     {".-", 'A'}, {"-...", 'B'}, {"-.-.", 'C'} /* autres....*/};
</syntaxhighlight>Ensuite nous avons définis différentes fonctions utilitaires afin de transformer les caractères en morse et inversement, de les lire, de mettre à jour des tableaux, comme en témoigne ces signatures de méthodes (leur documentation détaillée est disponible dans le code).<syntaxhighlight lang="c" line="1">
</syntaxhighlight>Ensuite nous avons définis différentes fonctions utilitaires afin de transformer les caractères en morse et inversement, de les lire, de mettre à jour des tableaux, comme en témoigne ces signatures de méthodes (leur documentation détaillée est disponible dans le code et nous n'allons pas nous attarder sur ceci).<syntaxhighlight lang="c" line="1">
const char *get_morse_from_char(char c);
const char *get_morse_from_char(char c);
char get_char_from_morse(const char *morse);
char get_char_from_morse(const char *morse);
Ligne 104 : Ligne 177 :
void initialize_morse_game(void);
void initialize_morse_game(void);
void use_buzzer(void);
void use_buzzer(void);
</syntaxhighlight>La partie la plus complexe réside finalement dans ces fonctions qui sont les principales du projet :<syntaxhighlight lang="c" line="1">
int main(void);
char translate_morse_to_char(bool reset_message);
bool translate_text_to_morse(const char *message, const char *morse_message, bool reset_message);
</syntaxhighlight>La fonction <code>translate_morse_to_char</code> permet de réaliser la traduction de morse en texte. C'est la fonction  qui est chargée de détecter lorsque le bouton est appuyé et la durée pendant laquelle il est appuyé. L'idée est que cette fonction soit appelée depuis la boucle principale et comme <syntaxhighlight lang="c" line="1">
/**
* @brief Converts button presses into Morse code and translates it into characters.
*
* This function interprets button presses as Morse code input, translates the Morse code
* into characters, and manages the state of the translation process. It also handles
* resetting the translation state when requested.
*
* @param reset_message If true, resets the translation state and clears buffers.
* @return The translated character if a complete Morse code sequence is detected,
*        or '\0' if no character is ready.
*/
char translate_morse_to_char(bool reset_message)
{
    static unsigned long button_time_pressed = 0;    ///< Duration the button has been pressed.
    static unsigned long button_time_not_pressed = 0; ///< Duration the button has not been pressed.
    static unsigned long elapsed_time = 0;            ///< Elapsed time since the last state change.
    static char morse_buffer[MAX_MORSE_CHAR_LEN]; ///< Buffer to store the current Morse code sequence.
    static int morse_index = 0;                  ///< Current index in the Morse buffer.
    static char message_buffer[MAX_MESSAGE_LEN]; ///< Buffer to store the translated message.
    static int index_message_array = 0;          ///< Current index in the message buffer.
    static bool has_been_pressed = false;    ///< Tracks if the button has been pressed.
    static bool has_not_been_pressed = false; ///< Tracks if the button has not been pressed.
    static bool was_reseted = false;          ///< Tracks if the translation state was reset.
    static char last_character; ///< Stores the last translated character.
    // Reset the translation state if requested
    if (reset_message)
    {
        reinitilize_array(message_buffer, sizeof(message_buffer));
        reinitilize_array(morse_buffer, sizeof(morse_buffer));
        index_message_array = 0;
        morse_index = 0;
        last_character = '\0';
        was_reseted = true;
        return '\0';
    }
    // Handle buffer overflow by shifting the message buffer
    if (index_message_array == MAX_MESSAGE_LEN)
    {
        int characters_to_display = MAX_MESSAGE_LEN / 2;
        for (int i = 0; i < characters_to_display; i++)
        {
            message_buffer[i] = message_buffer[i + characters_to_display];
        }
        index_message_array = characters_to_display;
        for (int i = characters_to_display; i < MAX_MESSAGE_LEN; i++)
        {
            message_buffer[i] = '\0';
        }
    }
    // Check if the button is not pressed
    if (PINC & B0)
    {
        PORTE &= ~LED1; // Turn off LED1
        has_been_pressed = false;
        if (!has_not_been_pressed)
        {
            has_not_been_pressed = true;
            elapsed_time = time_counter;
        }
        button_time_not_pressed = (time_counter - elapsed_time) * COUNTER_PERIOD;
        if (was_reseted)
        {
            was_reseted = false;
            button_time_pressed = 0;
        }
        // Handle Morse code input based on button press duration
        if (button_time_pressed != 0)
        {
            update_morse_array(morse_buffer, &morse_index, button_time_pressed < DASH_DURATION ? '.' : '-');
            button_time_pressed = 0;
        }
        else if (button_time_not_pressed > WORD_PAUSE && index_message_array > 0 && last_character != ' ')
        {
            last_character = ' ';
            update_message_array(message_buffer, &index_message_array, last_character);
            display_message_last_lines(message_buffer, index_message_array);
            return last_character;
        }
        else if (button_time_not_pressed > LETTER_PAUSE && morse_index > 0)
        {
            elapsed_time = time_counter;
            morse_buffer[morse_index] = '\0';
            last_character = get_char_from_morse(morse_buffer);
            reinitilize_array(morse_buffer, morse_index);
            morse_index = 0;
            update_message_array(message_buffer, &index_message_array, last_character);
            display_message_last_lines(message_buffer, index_message_array);
            return last_character;
        }
    }
    else
    {
        PORTE |= LED1; // Turn on LED1
        use_buzzer();
        has_not_been_pressed = false;
        if (!has_been_pressed)
        {
            has_been_pressed = true;
            elapsed_time = time_counter;
        }
        button_time_pressed = (time_counter - elapsed_time) * COUNTER_PERIOD;
        button_time_not_pressed = 0;
        // Reset the translation state if the button is held for a long duration
        if (button_time_pressed > RESET_PAUSE)
        {
            reinitilize_array(message_buffer, index_message_array);
            reinitilize_array(morse_buffer, morse_index);
            index_message_array = 0;
            morse_index = 0;
            last_character = '\0';
            display_message_last_lines(message_buffer, index_message_array);
            was_reseted = true;
        }
    }
    return '\0';
}
</syntaxhighlight>
Cette fonction est particulièrement complexe car on ne doit pas seulement compter le temps pendant lequel le bouton d'émission est appuyé par l'utilisateur mais également le temps pendant lequel le bouton n'est pas appuyé, car en fonction de ce temps là, nous saurons si un espace doit être placé après le dernier caractère (au delà de sept unités de temps écoulé il s'agit normalement d'un nouveau mot).
Pour ce faire, on procède au calcul du temps à chaque tour de boucle afin d'enregistrer le temps qui s'est écoulé depuis la dernière action :<syntaxhighlight lang="c" line="1">
button_time_not_pressed = (time_counter - elapsed_time) * COUNTER_PERIOD;
</syntaxhighlight>
Ensuite en fonction du temps d'appui on est capables de déterminer s'il sagit d'un point ou d'un trait. On peut alors placer tout cela dans un tableau de caractères, et une fois la saisie espacée par un grand blanc sans appui (3 unités de temps), on peut alors essayer de décoder la lettre entrée par l'utilisateur. On peut alors l'ajouter au message décodé, la fonction retourne la nouvelle lettre décodée et tout peut alors recommencer. Un espace est automatiquement placé dans la chaine après sept unités de temps sans appui.
Pour l'émission d'un message en morse, le fonctionnement est similaire.<syntaxhighlight lang="c" line="1">
/**
* @brief Converts a text message into Morse code and handles real-time display.
*
* This function translates a given text message into Morse code and manages the
* timing and display of the translation process. It supports resetting the translation
* state and handles the timing for Morse code signals (dots, dashes, and pauses).
*
* @param message The input text message to be translated.
* @param morse_message The output Morse code message.
* @param reset_message If true, resets the translation state and clears buffers.
* @return True if the translation is complete, false otherwise.
*/
bool translate_text_to_morse(const char *message, const char *morse_message, bool reset_message)
{
    static int translation_morse_index = 0;      ///< Current index in the Morse message being translated.
    static int message_letter_index = 0;          ///< Current index in the original text message.
    static unsigned long last_time = 0;          ///< Timestamp of the last translation step.
    static bool is_translating = true;            ///< Indicates if the translation is in progress.
    static bool is_new_letter = true;            ///< Indicates if a new letter is being processed.
    static char partial_message[MAX_MESSAGE_LEN]; ///< Buffer for the partially translated message.
    // Reset the translation state if requested
    if (reset_message)
    {
        translation_morse_index = 0;
        message_letter_index = 0;
        last_time = 0;
        is_translating = true;
        reinitilize_array(partial_message, sizeof(partial_message));
        return true;
    }
    // Initialize the last_time variable if it hasn't been set
    if (last_time == 0)
    {
        last_time = time_counter * COUNTER_PERIOD;
    }
    // Calculate the elapsed time since the last translation step
    unsigned long elapsed = time_counter * COUNTER_PERIOD - last_time;
    // Get the current Morse character being processed
    char current_char = morse_message[translation_morse_index];
    if (is_translating)
    {
        // Handle the active signal (dot or dash)
        if ((current_char == '-' && elapsed < DASH_DURATION) ||
            (current_char == '.' && elapsed < MORSE_PERIOD))
        {
            PORTE |= LED1; // Turn on LED1 to indicate an active signal
            use_buzzer();  // Activate the buzzer
            is_new_letter = true;
            return false;
        }
        // Handle pauses between letters or words
        else if ((current_char == ' ' && elapsed < LETTER_PAUSE - MORSE_PERIOD) ||
                (current_char == '/' && elapsed < WORD_PAUSE - MORSE_PERIOD))
        {
            PORTE &= ~LED1; // Turn off LED1 during pauses
            if (is_new_letter)
            {
                is_new_letter = false;
                message_letter_index += (current_char == '/') ? 2 : 1;
                strncpy(partial_message, message, message_letter_index);
                display_message_last_lines(partial_message, message_letter_index);
            }
            return false;
        }
        // Handle the end of the Morse message
        else if (current_char == '\0')
        {
            if (is_new_letter)
            {
                is_new_letter = false;
                message_letter_index += (current_char == '/') ? 2 : 1;
                strncpy(partial_message, message, message_letter_index);
                display_message_last_lines(partial_message, message_letter_index);
            }
            return true;
        }
        // Move to the next Morse character
        else
        {
            PORTE &= ~LED1; // Turn off LED1
            translation_morse_index++;
            is_translating = false;
            last_time = time_counter * COUNTER_PERIOD;
        }
    }
    else
    {
        // Wait for the pause duration before resuming translation
        if (elapsed >= MORSE_PERIOD)
        {
            is_translating = true;
            last_time = time_counter * COUNTER_PERIOD;
        }
    }
    return false;
}
</syntaxhighlight>
</syntaxhighlight>
La fonction reçoit une chaîne de caractères en entrée directement sou format morse, avec des lettre séparés par des espaces et les mots séparés par des slash, de cette manière :
<code>...- .. ...- . / .-.. . / ... -.-- ... - . -- . / . -- -... .- .-. --.- ..- . / -.-.--</code>
Ensuite, elle parcours chaque élément et de la même manière que dans l'autre fonction, procède à des relevés de temps et s'assure que pendant le nombre d'unités de temps necessaire, elle effectue la bonne action appropriée qui peut être :
* Emettre un son via le buzzer et allumer la led (point ou trait);
* Ne rien faire (temps d'espace entre les caractères, lettres et mots).
Enfin, ces deux fonctions sont appelées dont la fonction principale <code>main</code> qui initialise le programme et réalise les opérations d'entrée sorties. C'est cette fonction qui réalise une boucle infinie pendant laquelle va tourner le programme et donc réalise également le changement de mode de traduction à travers l'autre bouton. Elle s'occupe d'appeler la bonne fonction de traduction en fonction du mode sélectionné et réalise l'opération de lecture depuis l'entrée série USB. Pour cela, on a réalisé une fonctioon de lecture qui retourne le caractère lu, et cette fonction est appelé de nombreuses fois. Cela permet à l'utilisateur de rentrer du texte dans la console et ce texte est alors ajouté à une chaine qui pourra alors être traduit en morse. Le reste du temps, la saisie est désactivée pour des raisons pratiques. Lorsque la personne écrit en morse, les caractères déchiffrés qu'il tape sont écris au fur et à mesure dans le port série, via une fonction d'écriture. Elles sont décrites ci-dessous :<syntaxhighlight lang="c" line="1">
/** Function to manage CDC data transmission and reception to and from the host. */
void TX_CDC_Task(char *content)
{
/* Device must be connected and configured for the task to run */
if (USB_DeviceState != DEVICE_STATE_Configured)
return;
/* Flag management - Only allow one string to be sent per action */
if ((content != NULL && content[0] != '\0') && LineEncoding.BaudRateBPS)
{
/* Select the Serial Tx Endpoint */
Endpoint_SelectEndpoint(CDC_TX_EPADDR);
/* Write the String to the Endpoint */
Endpoint_Write_Stream_LE(content, strlen(content), NULL);
/* Remember if the packet to send completely fills the endpoint */
bool IsFull = (Endpoint_BytesInEndpoint() == CDC_TXRX_EPSIZE);
/* Finalize the stream transfer to send the last packet */
Endpoint_ClearIN();
/* If the last packet filled the endpoint, send an empty packet to release the buffer on
* the receiver (otherwise all data will be cached until a non-full packet is received) */
if (IsFull)
{
/* Wait until the endpoint is ready for another packet */
Endpoint_WaitUntilReady();
/* Send an empty packet to ensure that the host does not buffer data sent to it */
Endpoint_ClearIN();
}
}
}
char *RX_CDC_Task()
{
/* Select the OUT Endpoint */
Endpoint_SelectEndpoint(CDC_RX_EPADDR);
/* Check if Endpoint contains a packet */
if (Endpoint_IsOUTReceived())
{
/* Check to see if the packet contains data */
if (Endpoint_IsReadWriteAllowed())
{
/* Read from the host */
char *content = malloc(2);
for (int i = 0; i < 2; i++)
{
content[i] = '\0';
}
content[0] = Endpoint_Read_8();
content[1] = '\0';
/* Handshake the OUT Endpoint - clear endpoint and ready for next report */
Endpoint_ClearOUT();
return content;
}
/* Handshake the OUT Endpoint - clear endpoint and ready for next report */
Endpoint_ClearOUT();
}
return NULL;
}
</syntaxhighlight>Enfin, il restait une dernière chose à gérer qui est l'écran LCD. Nous avons utilisé une bibliothèque existante pour l'initialisation de l'écran, qui se trouve dans le dossier <code>lcd</code>. Cependant, les fonctions d'affichages comprenaient des ''delays'', ce qu'on cherche à éviter puisque le but est de ne pas ralentir le programme et se baser uniquement sur le temps du compteur incrémenté dans la fonction d'interruption. On a donc réécris nous-mêmes ces fonctions pour effacer, afficher le haut et le bas, avec un défilement, ce qui nous a permis par la suite de gérer la mémoire de manière effiente afin d'alouer uniquement uen chaîne de caractères de taille fixe dans laquelle nous avons simplement décalés nos caractères au fur et à mesure des saisies pour une gestion de la mémoire améliorée. Ces fonctions sont décrites ci-dessous :<syntaxhighlight lang="c" line="1">
/**
* @brief Displays a message on the LCD.
*
* This function writes a message to the LCD, filling the remaining space
* with blank characters if the message is shorter than the display size.
*
* @param message The message to display.
* @param message_size The size of the message.
*/
void display_message(char *message, size_t message_size)
{
    int max_chars = NB_ROWS * NB_COLS;
    for (int i = 0; i < max_chars; i++)
    {
        int address = HD44780_XY2Adrr(NB_ROWS, NB_COLS, i / NB_COLS, i % NB_COLS);
        HD44780_WriteCommand(LCD_ADDRSET | address);
        if (i > message_size || message[i] == '\0')
            HD44780_WriteData(' ');
        else
            HD44780_WriteData(message[i]);
    }
}
/**
* @brief Displays the last lines of a message on the LCD.
*
* This function calculates the starting point to display the last lines
* of a message, ensuring it fits within the LCD's dimensions.
*
* @param message The message to display.
* @param message_size The size of the message.
*/
void display_message_last_lines(char *message, size_t message_size)
{
    int total_lines = (message_size + NB_COLS - 1) / NB_COLS; // Nombre total de lignes dans le message
    // On prend les `max_lines` dernières lignes
    int start_line = total_lines > NB_ROWS ? total_lines - NB_ROWS : 0;
    int start_index = start_line * NB_COLS; // Premier caractère à afficher
    for (int i = 0; i < NB_ROWS * NB_COLS; i++)
    {
        int address = HD44780_XY2Adrr(NB_ROWS, NB_COLS, i / NB_COLS, i % NB_COLS);
        HD44780_WriteCommand(LCD_ADDRSET | address);
        // Calcul du bon index dans le message
        int message_index = start_index + i;
        if (message_index >= message_size || message[message_index] == '\0')
            HD44780_WriteData(' '); // Espace si on dépasse la fin du message
        else
            HD44780_WriteData(message[message_index]); // Affichage normal
    }
}
/**
* @brief Clears the LCD display.
*
* This function sends the clear display command to the LCD, resetting
* its content and cursor position.
*/
void clear_display(void)
{
    HD44780_WriteCommand(LCD_CLEAR);
}
</syntaxhighlight>D'autres fonctionnalités et améliorations ont été ajoutés dans ce projet même si elles n'ont pas été détaillées : variables statiques afin d'éviter les variables globales quand c'était possible et les fuites mémoires à cause des allocations mémoires, affichage propre dans la console du port série, gestion des caractères morses non affichables et inconnus dans le code morse international, limitation des saisies de messages pour éviter les ''overflows'', gestion des rebonds lors d'appuis de bouton, système d'écriture et de suppression dans la console, affichage des caractères traduis au fur et à mesure sur l'écran, etc.
Le code complet peut être trouvé et parcouru, il est disponible dans l'archive en bas de cette page.


= Lancement =
= Lancement =
Pour lancer le projet, il faut insérer la carte dans l'ordinateur et se placer dans le dossier <code>I2L/VirtualSerial</code> à la racine du projet. Depuis cet endroit, il faudra lancer la commande de compilation qui va appeler les makefiles et transférer le code dans la carte :
Pour lancer le projet, il faut insérer la carte dans l'ordinateur et se placer dans le dossier <code>I2L/VirtualSerial</code> à la racine du projet. Depuis cet endroit, il faudra lancer la commande de compilation qui va appeler les makefiles et transférer le code dans la carte :


<code>sudo make dfu</code>
<code>sudo make dfu</code>


Une fois cette commande effectuée vous serez en mesurer de texter en morse comme l'illustre les vidéos ci-dessous!
Une fois cette commande effectuée vous serez en mesurer de texter en morse comme l'illustre les vidéos ci-dessous!
= Démonstrations =
= Démonstrations =
 
Voici deux vidéo, la première montre comment il est possible d'écrire du texte en morse (même si c'est très dur <s>on ne dira pas qu'on a enregistré la vidéo plusieurs fois</s>) et la deuxième, ce que représente en morse un texte écris dans un port série USB.[[Fichier:I2L-2024-G4-text-to-morse-video.mp4|vignette|gauche]][[Fichier:I2L-2024-G4-morse-to-texte-video.mp4|vignette|centré]]
= Rendus =
= Rendus =



Version actuelle datée du 1 avril 2025 à 16:33

Proposition de système

Ce projet consiste à concevoir un simulateur de Morse basé sur un microcontrôleur ATmega32U4. Il permet la traduction :

  1. Morse → Texte : L'utilisateur entre un code Morse via un bouton poussoir, et la carte le convertit en texte affiché sur le PC via une connexion USB série. On peut imaginer également afficher en temps réel les caractères entrés dans un écran LCD, dans le cas où il n'y a aucune connexion avec un poste fixe.
  2. Texte → Morse : Le PC envoie un message via USB, et la carte le traduit en signaux lumineux (LED) et sonores (buzzer). On peut également imaginer un affichage sur l'écran LCD.

Le circuit comprend :

  • Un bouton poussoir pour saisir le Morse.
  • Une LED et un buzzer pour la restitution des signaux.
  • Un écran LCD pour afficher le message saisi en morse.
  • Une interface USB pour la communication avec un PC.

Contre-proposition

OK pour le système embarqué décrit.

Pas de difficulté de programmation au niveau du système embarqué pour le buzzer et le bouton (gestion classique d'entrée et de sortie).

Pour la gestion de l'écran LCD vous pouvez vous inspirer du code des groupes 3 et 4 I2L en 2023/2024.

Pour la gestion série au niveau du système embarqué vous pouvez vous inspirer du code du groupe 5 I2L en 2023/2024.

Proposition définitive

Système de communication en port série avec deux boutons : un pour changer le mode (transmission ou réception) et un pour écrire des données en mode transmission.

La led s'allumera en mode transmission et reception, soit lorsque l'utilisateur appuie sur le bouton en transmission, soit lors de la traduction en mode réception et le buzzer émettera du son en conséquence. Les caratères traduits seront envoyés dans le port USB série et l'écran LCD et le texte à chiffrer en morse sera entré par le port série et affiché au fur et à mesure de la traduction sur l'écran LCD. Un mode de reset permettera de supprimer le texte affiché sur l'écran LCD lorsque le bouton sera maintenu.

Les caractères ASCII et les caractères spéciaux seront gérés, sauf les accents et caractères qui ne font pas partis du code international du morse.

Répartition du travail

Maxime se concentre sur la partie du haut parleur et les boutons poussoirs.

Jérémy réalise le micro controlleur avec les LEDs.


Conclusion : nous avons travaillé tous les deux sur des sujets différents au départ. Jérémy a réussi à réaliser la transmission et la réception USB en port série et Maxime a géré l'affichage sur l'écran LCD. Pour le reste, nous avons travaillé à part égale, ensemble, sur toutes les fonctionnalités spécifiées.

Carte

Schéma initial

Schéma de notre montage
Schéma de notre montage

Carte routée

Schéma de la carte
vue de la carte

Composants

  • ATmega32u4 : disponible
  • quartz GND24 : disponible
  • buzzer : disponible
  • perle ferrite MH2029-300Y : commandée
  • chargeur MAX1811 : disponible
  • potentiomètre : disponible
  • connecteur femelle 16 contacts : commandé
  • écran LCD 2 lignes : commandé
  • boutons : disponibles

Carte au 23/02/2025

Début de réalisation

Non encore réalisé :

  • ajouter les connecteurs J5, J6, J7 et J9 pour la charge ;
  • ajouter le condensateur de 2,2uF pour la charge ;
  • ajouter R8 et le potentiomètre pour l'écran LCD ;
  • ajouter le buzzer.

Carte au 27/02/2025

Réalisation terminée

Carte finalisée avec l'écran LCD et prête à l'emploi.

Explication du fonctionnement du code morse

Pour comprendre la suite, il va nous falloir détailler comment fonctionne le morse, car la mesure du temps est primordiale pour comprendre comment fonctionnent la traduction et le fonctionnement.

Chaque lettre en morse correspond à une suite de points et de traits bien défini. Par exemple, le signe SOS s'écrirait ... --- ... en morse, puisque le S est composé de 3 points et le O de trois traits, comme inscrit dans le code international du morse.

Un point représente une unité de temps et un trait représente trois unités de temps. Chaque point ou trait est séparé d'une unité de temps. Chaque lettre est séparé de trois unités de temps soit l'équivalent d'un trait, et un mot est séparé d'un autre par sept unités de temps.

Cela signifie donc qu'un mot comme SOS prendrait 27 unités de temps + 7 autres unités de temps si un autre mot est émis ensuite. Pour arriver à ce résultat, il suffit d'aditionner chaque caractères :

  • 6 points = 6 unités de temps
  • 3 traits = 3 x 3 = 9 unités de temps
  • 6 unités de temps entre chaque caractère (sauf entre chaque lettre)
  • et 2 x 3 unités de temps séparant les trois lettres

Il nous fallait définir des constantes précises pour l'unité de temps, puis ensuite les calcul se baserai sur le temps défini, sachant que nous ne sommes pas des experts en morse, l'idéal est de mettre des temps élevés pour qu'on arrive à formuler des messages mais à l'époque il fallait aller vite puisque le but était d'écrire vite... Le véritable porblème réside donc dans cette mesure du temps. Nous avons dû utiliser le TIMER3 de notre carte afin d'avoir un compteur de temps qui s'incrémente régulièrement afin de pouvoir facilement détecter les caractères que souhaite faire l'utilisateur.

/**
 * @brief Initializes the hardware timer for periodic interrupts.
 *
 * @param diviseur Clock divider value.
 * @param periode Timer period in milliseconds.
 */
void initiliaze_timer(int diviseur, long periode)
{
    CLKSEL0 = 0b00010101; // sélection de l'horloge externe
    CLKSEL1 = 0b00001111; // minimum de 8Mhz
    CLKPR = 0b10000000;   // modification du diviseur d'horloge (CLKPCE=1)
    CLKPR = 0;            // 0 pour pas de diviseur (diviseur de 1)

    TCCR3A = 0;
    TCCR3B |= (1 << WGM32); // Le mode choisi n'utilise pas ce registre
    switch (diviseur)
    {
    case 8:
        TCCR3B |= (1 << CS31);
        break;
    case 64:
        TCCR3B |= (1 << CS31 | 11 << CS30);
        break;
    case 256:
        TCCR3B |= (1 << CS32);
        break;
    case 1024:
        TCCR3B |= (1 << CS32 | 1 << CS30);
        break;
    }
    // Un cycle prend 1/F_CPU secondes.
    // Un pas de compteur prend diviseur/F_CPU secondes.
    // Pour une periode en millisecondes, il faut (periode/1000)/(diviseur/F_CPU) pas
    // soit (periode*F_CPU)/(1000*diviseur)
    OCR3A = F_CPU / PERIOD * periode / diviseur; // Calcul du pas
    TCNT3 = 0;                                   // Compteur initialisé
    TIMSK3 = (1 << OCIE3A);                      // Comparaison du compteur avec OCR1A
}

unsigned long time_counter = 0; ///< Global time counter incremented by timer interrupts

/**
 * @brief Timer interrupt service routine.
 *
 * Increments the global time counter.
 */
ISR(TIMER3_COMPA_vect)
{
    time_counter++;
}

Enfin, il est bon de noter que puisque nous nous servions d'un timer pour mesurer le temps et d'un système de communication avec le port série, l'utilisation de delais dans le code était prohibé puisque nous ne devions pas ralentir l'execution du code et uniquement nous reposer sur cette variable compteur.

Avec ces informations en notre possession, nous étions capable d'avoir une mesure du temps précise qui nous permetterai de savoir si l'utilisateur cherche à faire un son court (appui court sur le bouton) ou un son long (appui long). Ces données sont également cruciales lors de l'émission d'un message morse par la carte, puisque celle-ci doit émettre un ensemble de sons longs et courts en préservant les bonnes durées d'espacement entre les lettres et les mots afin que le mot soit audible et compréhensible.

Nous pouvions nous atteler à l'implémentation de notre projet.

Code

Pour développer notre t, il nous fallait un moyen de représenter le code morse, et donc des structures :

typedef struct
{
    char character;
    const char *code;
} CharToMorseEntry;

typedef struct
{
    const char *code;
    char character;
} MorseToCharEntry;

Nous avons également définis les valeurs associées du code morse international afin de pouvoir faire la correspondance entre le français et le code morse associé et inversement (puisque chaque lettre a un code qui lui est propre) :

CharToMorseEntry char_to_morse_table[] = {
    {'A', ".-"}, {'B', "-..."}, {'C', "-.-."} /* autres....*/ }};

MorseToCharEntry morse_to_char_table[] = {
    {".-", 'A'}, {"-...", 'B'}, {"-.-.", 'C'} /* autres....*/};

Ensuite nous avons définis différentes fonctions utilitaires afin de transformer les caractères en morse et inversement, de les lire, de mettre à jour des tableaux, comme en témoigne ces signatures de méthodes (leur documentation détaillée est disponible dans le code et nous n'allons pas nous attarder sur ceci).

const char *get_morse_from_char(char c);
char get_char_from_morse(const char *morse);
void get_morse_from_text(const char *input, char *output, size_t max_size);
bool is_character_is_valid_morse(char c);
void update_morse_array(char *morse_array, int *char_index, char character);
void update_message_array(char *message_array, int *char_index, char character);
void reinitilize_array(char *array, int max_index);

Nous avons ajouté des fonctions pour contrôler la partie hardware du programme, tel que le buzzer, l'initialisation du timer, etc.

void initiliaze_timer(int diviseur, long periode);
void initialize_morse_game(void);
void use_buzzer(void);

La partie la plus complexe réside finalement dans ces fonctions qui sont les principales du projet :

int main(void);
char translate_morse_to_char(bool reset_message);
bool translate_text_to_morse(const char *message, const char *morse_message, bool reset_message);

La fonction translate_morse_to_char permet de réaliser la traduction de morse en texte. C'est la fonction qui est chargée de détecter lorsque le bouton est appuyé et la durée pendant laquelle il est appuyé. L'idée est que cette fonction soit appelée depuis la boucle principale et comme

/**
 * @brief Converts button presses into Morse code and translates it into characters.
 *
 * This function interprets button presses as Morse code input, translates the Morse code
 * into characters, and manages the state of the translation process. It also handles
 * resetting the translation state when requested.
 *
 * @param reset_message If true, resets the translation state and clears buffers.
 * @return The translated character if a complete Morse code sequence is detected,
 *         or '\0' if no character is ready.
 */
char translate_morse_to_char(bool reset_message)
{
    static unsigned long button_time_pressed = 0;     ///< Duration the button has been pressed.
    static unsigned long button_time_not_pressed = 0; ///< Duration the button has not been pressed.
    static unsigned long elapsed_time = 0;            ///< Elapsed time since the last state change.

    static char morse_buffer[MAX_MORSE_CHAR_LEN]; ///< Buffer to store the current Morse code sequence.
    static int morse_index = 0;                   ///< Current index in the Morse buffer.

    static char message_buffer[MAX_MESSAGE_LEN]; ///< Buffer to store the translated message.
    static int index_message_array = 0;          ///< Current index in the message buffer.

    static bool has_been_pressed = false;     ///< Tracks if the button has been pressed.
    static bool has_not_been_pressed = false; ///< Tracks if the button has not been pressed.
    static bool was_reseted = false;          ///< Tracks if the translation state was reset.

    static char last_character; ///< Stores the last translated character.

    // Reset the translation state if requested
    if (reset_message)
    {
        reinitilize_array(message_buffer, sizeof(message_buffer));
        reinitilize_array(morse_buffer, sizeof(morse_buffer));
        index_message_array = 0;
        morse_index = 0;
        last_character = '\0';
        was_reseted = true;
        return '\0';
    }

    // Handle buffer overflow by shifting the message buffer
    if (index_message_array == MAX_MESSAGE_LEN)
    {
        int characters_to_display = MAX_MESSAGE_LEN / 2;
        for (int i = 0; i < characters_to_display; i++)
        {
            message_buffer[i] = message_buffer[i + characters_to_display];
        }
        index_message_array = characters_to_display;
        for (int i = characters_to_display; i < MAX_MESSAGE_LEN; i++)
        {
            message_buffer[i] = '\0';
        }
    }

    // Check if the button is not pressed
    if (PINC & B0)
    {
        PORTE &= ~LED1; // Turn off LED1
        has_been_pressed = false;

        if (!has_not_been_pressed)
        {
            has_not_been_pressed = true;
            elapsed_time = time_counter;
        }

        button_time_not_pressed = (time_counter - elapsed_time) * COUNTER_PERIOD;

        if (was_reseted)
        {
            was_reseted = false;
            button_time_pressed = 0;
        }

        // Handle Morse code input based on button press duration
        if (button_time_pressed != 0)
        {
            update_morse_array(morse_buffer, &morse_index, button_time_pressed < DASH_DURATION ? '.' : '-');
            button_time_pressed = 0;
        }
        else if (button_time_not_pressed > WORD_PAUSE && index_message_array > 0 && last_character != ' ')
        {
            last_character = ' ';
            update_message_array(message_buffer, &index_message_array, last_character);
            display_message_last_lines(message_buffer, index_message_array);
            return last_character;
        }
        else if (button_time_not_pressed > LETTER_PAUSE && morse_index > 0)
        {
            elapsed_time = time_counter;
            morse_buffer[morse_index] = '\0';
            last_character = get_char_from_morse(morse_buffer);
            reinitilize_array(morse_buffer, morse_index);
            morse_index = 0;
            update_message_array(message_buffer, &index_message_array, last_character);
            display_message_last_lines(message_buffer, index_message_array);
            return last_character;
        }
    }
    else
    {
        PORTE |= LED1; // Turn on LED1
        use_buzzer();
        has_not_been_pressed = false;
        if (!has_been_pressed)
        {
            has_been_pressed = true;
            elapsed_time = time_counter;
        }
        button_time_pressed = (time_counter - elapsed_time) * COUNTER_PERIOD;
        button_time_not_pressed = 0;

        // Reset the translation state if the button is held for a long duration
        if (button_time_pressed > RESET_PAUSE)
        {
            reinitilize_array(message_buffer, index_message_array);
            reinitilize_array(morse_buffer, morse_index);
            index_message_array = 0;
            morse_index = 0;
            last_character = '\0';
            display_message_last_lines(message_buffer, index_message_array);
            was_reseted = true;
        }
    }

    return '\0';
}

Cette fonction est particulièrement complexe car on ne doit pas seulement compter le temps pendant lequel le bouton d'émission est appuyé par l'utilisateur mais également le temps pendant lequel le bouton n'est pas appuyé, car en fonction de ce temps là, nous saurons si un espace doit être placé après le dernier caractère (au delà de sept unités de temps écoulé il s'agit normalement d'un nouveau mot).

Pour ce faire, on procède au calcul du temps à chaque tour de boucle afin d'enregistrer le temps qui s'est écoulé depuis la dernière action :

button_time_not_pressed = (time_counter - elapsed_time) * COUNTER_PERIOD;

Ensuite en fonction du temps d'appui on est capables de déterminer s'il sagit d'un point ou d'un trait. On peut alors placer tout cela dans un tableau de caractères, et une fois la saisie espacée par un grand blanc sans appui (3 unités de temps), on peut alors essayer de décoder la lettre entrée par l'utilisateur. On peut alors l'ajouter au message décodé, la fonction retourne la nouvelle lettre décodée et tout peut alors recommencer. Un espace est automatiquement placé dans la chaine après sept unités de temps sans appui.

Pour l'émission d'un message en morse, le fonctionnement est similaire.

/**
 * @brief Converts a text message into Morse code and handles real-time display.
 *
 * This function translates a given text message into Morse code and manages the
 * timing and display of the translation process. It supports resetting the translation
 * state and handles the timing for Morse code signals (dots, dashes, and pauses).
 *
 * @param message The input text message to be translated.
 * @param morse_message The output Morse code message.
 * @param reset_message If true, resets the translation state and clears buffers.
 * @return True if the translation is complete, false otherwise.
 */
bool translate_text_to_morse(const char *message, const char *morse_message, bool reset_message)
{
    static int translation_morse_index = 0;       ///< Current index in the Morse message being translated.
    static int message_letter_index = 0;          ///< Current index in the original text message.
    static unsigned long last_time = 0;           ///< Timestamp of the last translation step.
    static bool is_translating = true;            ///< Indicates if the translation is in progress.
    static bool is_new_letter = true;             ///< Indicates if a new letter is being processed.
    static char partial_message[MAX_MESSAGE_LEN]; ///< Buffer for the partially translated message.

    // Reset the translation state if requested
    if (reset_message)
    {
        translation_morse_index = 0;
        message_letter_index = 0;
        last_time = 0;
        is_translating = true;
        reinitilize_array(partial_message, sizeof(partial_message));
        return true;
    }

    // Initialize the last_time variable if it hasn't been set
    if (last_time == 0)
    {
        last_time = time_counter * COUNTER_PERIOD;
    }

    // Calculate the elapsed time since the last translation step
    unsigned long elapsed = time_counter * COUNTER_PERIOD - last_time;

    // Get the current Morse character being processed
    char current_char = morse_message[translation_morse_index];

    if (is_translating)
    {
        // Handle the active signal (dot or dash)
        if ((current_char == '-' && elapsed < DASH_DURATION) ||
            (current_char == '.' && elapsed < MORSE_PERIOD))
        {
            PORTE |= LED1; // Turn on LED1 to indicate an active signal
            use_buzzer();  // Activate the buzzer
            is_new_letter = true;
            return false;
        }
        // Handle pauses between letters or words
        else if ((current_char == ' ' && elapsed < LETTER_PAUSE - MORSE_PERIOD) ||
                 (current_char == '/' && elapsed < WORD_PAUSE - MORSE_PERIOD))
        {
            PORTE &= ~LED1; // Turn off LED1 during pauses
            if (is_new_letter)
            {
                is_new_letter = false;
                message_letter_index += (current_char == '/') ? 2 : 1;
                strncpy(partial_message, message, message_letter_index);
                display_message_last_lines(partial_message, message_letter_index);
            }
            return false;
        }
        // Handle the end of the Morse message
        else if (current_char == '\0')
        {
            if (is_new_letter)
            {
                is_new_letter = false;
                message_letter_index += (current_char == '/') ? 2 : 1;
                strncpy(partial_message, message, message_letter_index);
                display_message_last_lines(partial_message, message_letter_index);
            }
            return true;
        }
        // Move to the next Morse character
        else
        {
            PORTE &= ~LED1; // Turn off LED1
            translation_morse_index++;
            is_translating = false;
            last_time = time_counter * COUNTER_PERIOD;
        }
    }
    else
    {
        // Wait for the pause duration before resuming translation
        if (elapsed >= MORSE_PERIOD)
        {
            is_translating = true;
            last_time = time_counter * COUNTER_PERIOD;
        }
    }
    return false;
}

La fonction reçoit une chaîne de caractères en entrée directement sou format morse, avec des lettre séparés par des espaces et les mots séparés par des slash, de cette manière :

...- .. ...- . / .-.. . / ... -.-- ... - . -- . / . -- -... .- .-. --.- ..- . / -.-.--

Ensuite, elle parcours chaque élément et de la même manière que dans l'autre fonction, procède à des relevés de temps et s'assure que pendant le nombre d'unités de temps necessaire, elle effectue la bonne action appropriée qui peut être :

  • Emettre un son via le buzzer et allumer la led (point ou trait);
  • Ne rien faire (temps d'espace entre les caractères, lettres et mots).

Enfin, ces deux fonctions sont appelées dont la fonction principale main qui initialise le programme et réalise les opérations d'entrée sorties. C'est cette fonction qui réalise une boucle infinie pendant laquelle va tourner le programme et donc réalise également le changement de mode de traduction à travers l'autre bouton. Elle s'occupe d'appeler la bonne fonction de traduction en fonction du mode sélectionné et réalise l'opération de lecture depuis l'entrée série USB. Pour cela, on a réalisé une fonctioon de lecture qui retourne le caractère lu, et cette fonction est appelé de nombreuses fois. Cela permet à l'utilisateur de rentrer du texte dans la console et ce texte est alors ajouté à une chaine qui pourra alors être traduit en morse. Le reste du temps, la saisie est désactivée pour des raisons pratiques. Lorsque la personne écrit en morse, les caractères déchiffrés qu'il tape sont écris au fur et à mesure dans le port série, via une fonction d'écriture. Elles sont décrites ci-dessous :

/** Function to manage CDC data transmission and reception to and from the host. */
void TX_CDC_Task(char *content)
{
	/* Device must be connected and configured for the task to run */
	if (USB_DeviceState != DEVICE_STATE_Configured)
		return;

	/* Flag management - Only allow one string to be sent per action */
	if ((content != NULL && content[0] != '\0') && LineEncoding.BaudRateBPS)
	{
		/* Select the Serial Tx Endpoint */
		Endpoint_SelectEndpoint(CDC_TX_EPADDR);

		/* Write the String to the Endpoint */
		Endpoint_Write_Stream_LE(content, strlen(content), NULL);

		/* Remember if the packet to send completely fills the endpoint */
		bool IsFull = (Endpoint_BytesInEndpoint() == CDC_TXRX_EPSIZE);

		/* Finalize the stream transfer to send the last packet */
		Endpoint_ClearIN();

		/* If the last packet filled the endpoint, send an empty packet to release the buffer on
		 * the receiver (otherwise all data will be cached until a non-full packet is received) */
		if (IsFull)
		{
			/* Wait until the endpoint is ready for another packet */
			Endpoint_WaitUntilReady();

			/* Send an empty packet to ensure that the host does not buffer data sent to it */
			Endpoint_ClearIN();
		}
	}
}

char *RX_CDC_Task()
{
	/* Select the OUT Endpoint */
	Endpoint_SelectEndpoint(CDC_RX_EPADDR);

	/* Check if Endpoint contains a packet */
	if (Endpoint_IsOUTReceived())
	{
		/* Check to see if the packet contains data */
		if (Endpoint_IsReadWriteAllowed())
		{
			/* Read from the host */
			char *content = malloc(2);
			for (int i = 0; i < 2; i++)
			{
				content[i] = '\0';
			}
			content[0] = Endpoint_Read_8();
			content[1] = '\0';
			/* Handshake the OUT Endpoint - clear endpoint and ready for next report */
			Endpoint_ClearOUT();
			return content;
		}
		/* Handshake the OUT Endpoint - clear endpoint and ready for next report */
		Endpoint_ClearOUT();
	}
	return NULL;
}

Enfin, il restait une dernière chose à gérer qui est l'écran LCD. Nous avons utilisé une bibliothèque existante pour l'initialisation de l'écran, qui se trouve dans le dossier lcd. Cependant, les fonctions d'affichages comprenaient des delays, ce qu'on cherche à éviter puisque le but est de ne pas ralentir le programme et se baser uniquement sur le temps du compteur incrémenté dans la fonction d'interruption. On a donc réécris nous-mêmes ces fonctions pour effacer, afficher le haut et le bas, avec un défilement, ce qui nous a permis par la suite de gérer la mémoire de manière effiente afin d'alouer uniquement uen chaîne de caractères de taille fixe dans laquelle nous avons simplement décalés nos caractères au fur et à mesure des saisies pour une gestion de la mémoire améliorée. Ces fonctions sont décrites ci-dessous :

/**
 * @brief Displays a message on the LCD.
 * 
 * This function writes a message to the LCD, filling the remaining space
 * with blank characters if the message is shorter than the display size.
 * 
 * @param message The message to display.
 * @param message_size The size of the message.
 */
void display_message(char *message, size_t message_size)
{
    int max_chars = NB_ROWS * NB_COLS;
    for (int i = 0; i < max_chars; i++)
    {
        int address = HD44780_XY2Adrr(NB_ROWS, NB_COLS, i / NB_COLS, i % NB_COLS);
        HD44780_WriteCommand(LCD_ADDRSET | address);
        if (i > message_size || message[i] == '\0')
            HD44780_WriteData(' ');
        else
            HD44780_WriteData(message[i]);
    }
}

/**
 * @brief Displays the last lines of a message on the LCD.
 * 
 * This function calculates the starting point to display the last lines
 * of a message, ensuring it fits within the LCD's dimensions.
 * 
 * @param message The message to display.
 * @param message_size The size of the message.
 */
void display_message_last_lines(char *message, size_t message_size)
{
    int total_lines = (message_size + NB_COLS - 1) / NB_COLS; // Nombre total de lignes dans le message

    // On prend les `max_lines` dernières lignes
    int start_line = total_lines > NB_ROWS ? total_lines - NB_ROWS : 0;
    int start_index = start_line * NB_COLS; // Premier caractère à afficher

    for (int i = 0; i < NB_ROWS * NB_COLS; i++)
    {
        int address = HD44780_XY2Adrr(NB_ROWS, NB_COLS, i / NB_COLS, i % NB_COLS);
        HD44780_WriteCommand(LCD_ADDRSET | address);

        // Calcul du bon index dans le message
        int message_index = start_index + i;

        if (message_index >= message_size || message[message_index] == '\0')
            HD44780_WriteData(' '); // Espace si on dépasse la fin du message
        else
            HD44780_WriteData(message[message_index]); // Affichage normal
    }
}

/**
 * @brief Clears the LCD display.
 * 
 * This function sends the clear display command to the LCD, resetting
 * its content and cursor position.
 */
void clear_display(void)
{
    HD44780_WriteCommand(LCD_CLEAR);
}

D'autres fonctionnalités et améliorations ont été ajoutés dans ce projet même si elles n'ont pas été détaillées : variables statiques afin d'éviter les variables globales quand c'était possible et les fuites mémoires à cause des allocations mémoires, affichage propre dans la console du port série, gestion des caractères morses non affichables et inconnus dans le code morse international, limitation des saisies de messages pour éviter les overflows, gestion des rebonds lors d'appuis de bouton, système d'écriture et de suppression dans la console, affichage des caractères traduis au fur et à mesure sur l'écran, etc.

Le code complet peut être trouvé et parcouru, il est disponible dans l'archive en bas de cette page.

Lancement

Pour lancer le projet, il faut insérer la carte dans l'ordinateur et se placer dans le dossier I2L/VirtualSerial à la racine du projet. Depuis cet endroit, il faudra lancer la commande de compilation qui va appeler les makefiles et transférer le code dans la carte :

sudo make dfu

Une fois cette commande effectuée vous serez en mesurer de texter en morse comme l'illustre les vidéos ci-dessous!

Démonstrations

Voici deux vidéo, la première montre comment il est possible d'écrire du texte en morse (même si c'est très dur on ne dira pas qu'on a enregistré la vidéo plusieurs fois) et la deuxième, ce que représente en morse un texte écris dans un port série USB.

Rendus

Projet KiCAD : Fichier:I2L-2024-Carte-G4-rex.zip

Programmes : Fichier:I2L-2024-Programmes-G4.zip