« SE4Binome2025-5 » : différence entre les versions
Aucun résumé des modifications |
Aucun résumé des modifications |
||
| Ligne 574 : | Ligne 574 : | ||
Voici la vidéo démonstrative: | Voici la vidéo démonstrative: | ||
[[Fichier: | [[Fichier:DuoMontage2.mp4|néant|vignette]] | ||
En regardant sur un logiciel de montage, on voit que l'image s'écrit en ~6s sur l'écran, c'est plutôt cohérent avec le calcul, notamment car il faut prendre en compte l'envoi des données en SPI et l'affichage. | En regardant sur un logiciel de montage, on voit que l'image s'écrit en ~6s sur l'écran, c'est plutôt cohérent avec le calcul, notamment car il faut prendre en compte l'envoi des données en SPI et l'affichage. | ||
Version actuelle datée du 24 décembre 2025 à 02:14
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:
PCB du bouclier
Ci dessous le PCB du 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é.
Vue 3D des deux cartes
Ci-dessous la vue 3D de la carte du bouclier 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 MΩ
- 2 Résistances de 1 KΩ
- 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
PCB de la carte
Ci-dessous le PCB de la carte imprimée:
et ci dessous un zoom sur la partie logique:
Vue 3D de la carte
Ci-dessous une vue 3D de la carte fille:
A noter que le connecteur en bas de la carte(connecteur mère-fille) sera bien à l'horizontale.
Software
Ordonnanceur
Initialisation de la pile
Cette fonction initialise la pile d’un processus en y plaçant l’adresse de démarrage et en simulant le contexte sauvegardé lors d’une interruption, afin que le processus puisse être restauré et exécuté correctement par l’ordonnanceur.
void InitStack(int process_id){
uint16_t old_sp = SP;
SP = process_list[process_id].stack_pointer;
uint16_t address = (uint16_t)(process_list[process_id].process_address);
asm volatile("push %0" : : "r" (address & 0x00ff) );
asm volatile("push %0" : : "r" ((address & 0xff00)>>8) );
// Ensuite, simuler les registres empilés lors d'une vraie ISR
SAVE_REGISTERS();
process_list[process_id].stack_pointer = SP;
SP = old_sp;
}
Interruption nue
Cette interruption sauvegarde le contexte du processus courant, appelle l’ordonnanceur pour sélectionner la prochaine tâche, puis restaure le contexte de la tâche élue avant de reprendre l’exécution.
ISR(TIMER1_COMPA_vect, ISR_NAKED){ // Procédure d'interruption
/* Sauvegarde du contexte de la tâche interrompue */
SAVE_REGISTERS();
process_list[current_process].stack_pointer = SP;
/* Appel à l'ordonnanceur */
scheduler();
/* Récupération du contexte de la tâche ré-activée */
SP = process_list[current_process].stack_pointer;
RESTORE_REGISTERS();
asm volatile ( "reti" );
}
Ici, on va venir initialiser notre timer pour générer une interruption toutes les ~20ms.
void timer1_init(void) {
TCCR1A = 0;
TCCR1B = (1 << WGM12); // automatic reset mode
TCCR1B |= (1 << CS12) | (1 << CS10); // prescaler 1024
OCR1A = NB_CLICKS;
TIMSK1 = (1 << OCIE1A); // enable Compare A interrupt
TCNT1 = 0;
}
Structure d'un processus
typedef struct tab_process{
uint16_t stack_pointer;
void (*process_address)(void);
enum TaskState state;
uint16_t sleeping_time;
uint8_t id;
}Process;
Grâce à cette structure, on crée un tableau de processus pour stocker tous nos processus avec leurs différentes utilité.
extern Process process_list[MAX_PROCESS];
Fonction wait
Le problème d'utiliser _delay_ms, c'est qu'il est bloquant et donc qu'on ne peut pas utiliser le processeur pendant ce temps d'attente. Ce n'est pas optimal et donc on utilise une state machine à 2 états pour gérer si une tâche dort ou non.
void start_wait(uint16_t time){ // ms
if (process_list[current_process].state == AWAKE){
process_list[current_process].sleeping_time = time;
process_list[current_process].state = IDLE;
}
TIMER1_COMPA_vect(); // Fais sortir de la fonction en faisant un reset, évite de continuer à éxecuter le code
}
Avec ce système, si une tâche est endormie, on ne la lance pas.
Code de l'ordonnanceur
Parlons de la fonction scheduler, élément central de notre ordonnanceur puisque c'est ici que le choix des tâches à exécuter va être fait.
void scheduler(void)
{
decrement_sleeping_times();
next_process();
}
Regardons de plus près ces deux fonctions.
La première, decrement_sleeping_times, sert à retirer du temps à toutes les tâches endormies pour les rapprocher de leur réveil.
La deuxième, next_process, sert à choisir le prochain processus à démarrer.
Il faut pour cela deux conditions. : 1. Le processus doit être non nul. 2. Le processus doit être en état AWAKE
void next_process(void) {
int next = current_process;
do {
next = (next + 1) % MAX_PROCESS;
} while (process_list[next].process_address == NULL || process_list[next].state == IDLE);
current_process = next;
}
Un problème courant avec les délais non bloquants, c'est que toutes les tâches sont endormies, et donc le scheduler ne sait plus quelle tâche choisir. Pour éviter ce problème d'indécision, une tâche fantôme sera toujours présente.
void FantomTask(void)
{
while (1)
{
_delay_ms(30);
}
}
Exemple de tâches
Une tâche basique est de faire clignoter une LED.
void Led2(void)
{
while (1)
{
active_o(LED_2, 2, 'b');
start_wait(200);
}
}
Une tâche un peu plus complexe est la gestion de commandes via UART. La première phase du code consiste à récupérer une commande jusqu'à recevoir un \n ou un \r. Ensuite, on va venir vérifier si cette commande correspond à add, rm, ps ou progs. En fonction des cas de figures, des messages textuels sont envoyés pour communiquer avec l'utilisateur.
void SerialManager(){
while(1){
char buffer[16];
char commandStr[8];
unsigned char commandChar ;
...
do{
commandChar = USART_Receive();
sprintf(buffer, "%c", commandChar);
send_data(buffer);
if (commandChar == '\n' || commandChar == '\r' || commandChar == 13)
break;
if (i < sizeof(commandStr)-1)
commandStr[i++] = commandChar;
}while(1);
commandStr[i] = '\0';
send_data("\n");
if ( !strcmp(commandStr , "add") ){ //ADD PROCESSES
...//L'utilisateur saisit l'adresse d'un processus à ajouter
}else if( !strcmp(commandStr , "rm") ){ //REMOVE PROCESSES
...//L'utilisateur saisit l'adresse d'un processus à retirer
}
else if ( !strcmp(commandStr , "ps") ){ //ACTIVE PROCESSES STATUS
//AFFICHE TOUS LES PROCESSUS ACTIFS (Endormis ou non)
for(int i = 0; i < MAX_PROCESS ; i++){
if (process_list[i].process_address != NULL){
send_data("ID process:");
sprintf(buffer, "%d", process_list[i].id);
send_data(buffer);
send_data("\r\n");
send_data("Address:");
sprintf(buffer, "%p", process_list[i].process_address);
send_data(buffer);
send_data("\r\n");
const char * FunctionStr = getNameByAddressFunction(process_list[i].process_address);
...
send_data("Name:");
sprintf(buffer, "%s\r\n\n", FunctionStr);
send_data(buffer);
}
}
}else if ( !strcmp(commandStr , "progs") ){ //MAP PROCESSES
//AFFICHE TOUS LES PROCESSUS EXISTANTS
for(int i = 0; i < MAX_PROCESS ; i++){
if (strcmp(maps_processes[i].name, "")){
send_data("Name:");
sprintf(buffer, "%s\r\n", maps_processes[i].name);
send_data(buffer);
send_data("Address:");
sprintf(buffer, "%p\r\n", maps_processes[i].process_address);
send_data(buffer);
}
}
}
....
}
}
Ajout et retrait de processus
Voici le code pour ajouter un processus.
void add_process(void (*_process_address)(void),unsigned char initstack){
for (int i=0; i<MAX_PROCESS; i++){
if (process_list[i].process_address == NULL){
process_list[i].process_address = _process_address;
process_list[i].stack_pointer = INITIAL_STACK_ADDRESS - PROCESS_SIZE * (nb_process+1);
process_list[i].state = AWAKE;
process_list[i].id = i;
if(initstack) InitStack(i);
nb_process++;
break;
}
}
}
A première vue, on peut se demander pourquoi il y a une boucle for dans un processus d'ajout. Cette boucle cherche simplement un processus avec une adresse NULL dans le tableau, ce qui correspond à un processus vide. Avec ce système, on peut alors très aisément supprimer un processus, puisqu'il suffira de mettre sa variable process_address à NULL.
Et comme l'ordonnanceur, ne lancera jamais une tâche qui a process_address valant NULL, on garde un code simple et avec un système d'ajout et de retrait.
Pour finir, voici le code du retrait de processus :
void remove_process(int process_id){
if (process_list[process_id].process_address != NULL){
process_list[process_id].process_address = NULL;
nb_process -=1;
}
}
Exemple d'utilisation
Voici un exemple de main qui fonctionne et qui initialise plusieurs tâches
int main(void){
USART_Init(MYUBRR);
init_io();
init_processes();
add_process(FantomTask,0);
add_process(SerialManager, 1);
add_process(Led3,1);
add_process(Led4,1);
//remove_process(4);
add_process(Led5,1);
add_process(Led6,1);
timer1_init();
SP = process_list[current_process].stack_pointer;
sei();
process_list[current_process].process_address(); // Lancer le premier processus
while (1) ;
}
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.
Par ailleurs, ce n'est pas visible sur la vidéo mais nous avons réussi à utiliser correctement les options d'ajouts et de retrait de tâches ou encore d'affichage des tâches en cours, tout en ayant l'écran connecté. Cependant, même avec la police la plus petite, l'écran a une taille limitée, ce qui rendait la lecture peu pratique. Nous avons donc voulu créer un montage, plus visuel, plus intuitif, qui permet de montrer facilement notre travail.
Montage Duo
Ce montage davantage parlant, c'est celui-ci, il combine nos travaux pour permettre l'utilisation de l'écran de façon différentes en fonction de la tension d'entrée du pin PC3 de la carte mère. En effet, en reliant PC3 à GND, l'écran démarrera dans son mode classique d'affichage, avec un flux de caractères qui peut lui être envoyé via UART.
En reliant PC3 à 5V, l'écran passera désormais dans un mode de réception de flux également, mais celui d'une image. Pour afficher une image, il faut une quantité de donnée importante, et comme nous avons décidé d'envoyer par UART grâce à notre script sendImage.py, nous devons augmenter le baudrate. Nous le passons donc à 921600 au lieu de 9600 précédemment. Grâce à ce débit rapide, nous pouvons envoyer une image en peu de temps.
Calcul du temps d'envoi :
Dans notre cas, il y a 153600 pixels. Pour chacun d'entre eux, on envoie 3 octets.
Avec un baudrate de 9600, la transmission d'une image dure 384 secondes.
Avec un baudrate de 921600 , la transmission d'une image dure 4 secondes.
La différence est énorme !
Voici la vidéo démonstrative:
En regardant sur un logiciel de montage, on voit que l'image s'écrit en ~6s sur l'écran, c'est plutôt cohérent avec le calcul, notamment car il faut prendre en compte l'envoi des données en SPI et l'affichage.
Ce montage là n'utilise pas la RAM SPI, mais on aurait pu aussi l'utiliser.
Pour réutiliser notre montage, voici la démarche à suivre.
- Faire make flash sur la carte mère depuis le répertoire Software/DualMode de notre Gitea.
- Faire make upload via ICSP sur la carte fille depuis le répertoire Software/GPU.
- Monter le shield sur l'Arduino. Choisir le mode en reliant PC3 à GND ou 5V.
- Brancher la carte fille sur le J1 de la carte mère en s'assurant du sens de branchement grâces aux flèches blanches sur les PCB
- Dans le mode d'envoi d'image, il suffit d'exécuter le programme python disponible dans le répertoire Software/Images.
- Toute image peut être transmise, tant qu'elle est au préalable convertie en .bmp dans les dimensions de l'écran (480x320). Une image polytech.bmp est déjà prête à être envoyée.