« SE4Binome2023-1 » : différence entre les versions
Ligne 372 : | Ligne 372 : | ||
| style="width:50%;" |[[Fichier:Carte LCD non soudée.jpg|centré|vignette|300x300px|<div style="text-align:center;">Carte non soudée</div>]] | | style="width:50%;" |[[Fichier:Carte LCD non soudée.jpg|centré|vignette|300x300px|<div style="text-align:center;">Carte non soudée</div>]] | ||
| style="width:50%;" |[[Fichier:Carte LCD soudee.jpg|centré|vignette|400x400px|<div style="text-align:center;">Carte soudée</div>]] | | style="width:50%;" |[[Fichier:Carte LCD soudee.jpg|centré|vignette|400x400px|<div style="text-align:center;">Carte soudée</div>]] | ||
|[[Fichier:Video TestLCD.mp4|centré|vignette|<div style="text-align:center;">Video Test leds</div>]] | |[[Fichier:Video TestLCD.mp4|centré|vignette|<div style="text-align:center;"><div style="transform: rotate(90deg);">Video Test leds</div>]] | ||
|} | |} |
Version du 16 janvier 2024 à 18:42
Ordonnanceur
Soudure et Test du SHIELD
Au cours des 2 premières séances nous avons soudé et testé notre Shield
Sur l'un des connecteurs, la led est abîmée dû à une soudure excessive. Sachant que les leds sont uniquement utilisées en tant qu'indicateurs pour savoir quelle carte fille à le contrôle et que nous n'en utiliserons pas plus de 3 pour tester nos programmes, cela ne vas pas avoir d'influence négative. Le potentiomètre permet de régler le niveau de luminosité de l'écran et le connecteur HE10 permettra de comuniquer avec l'écran. Les problèmes rencontrés sont les suivants : Au départ, la carte ne fonctionnait pas, le soucis étant le quartz mal soudé, c'est à l'aide d'un pistolet à air chaud que celui-ci à été ressoudé. Un autre problème était le reset de la carte fille qui ne fonctionnait pas non plus, une ressoudure classique a permis de le faire fonctionner.
Code | Image |
---|---|
#include <avr/io.h> void init_led(){ DDRD=0x92; DDRC=0x09; } int main(){ init_led(); while(1) { PORTD=0x92; PORTC=0x09; } return 0; } |
Puis nous avons aussi soudé l'adaptateur HE10 pour l'utilisation de la matrice de leds
Programmation de l'Ordonnanceur
Nous commençons maintenant l'ordonnanceur, tout d'abord nous créeons la structure de nos Taches.
Code |
---|
struct TacheInfo{ void (*depart)(void); uint16_t SPointeur; int etat; }; |
Puis nous ajoutons les 2 premières taches demandées qui sont de faire clignoter 2 leds différentes à des timing différents.
Code |
---|
void TacheA(){ while(1) { PORTC ^= 0x08; _delay_ms(300); } } void TacheB(){ while(1) { PORTD ^= 0x80; _delay_ms(500); } } struct TacheInfo Taches[2]={ {TacheA,0x0600,0}, {TacheB,0x0700,0} }; |
On s'occupe maintenant de l'ordonnanceur, ainsi que de l'ISR.
Code |
---|
void ordonnanceur () { PORTD ^= 0x02; courant++; if (courant == NBR_TACHE) courant = 0; } ISR(TIMER1_COMPA_vect,ISR_NAKED) { /* Sauvegarde du contexte de la tâche interrompue */ SAVE_REGISTER(); Taches[courant].SPointeur = SP; /* Appel à l'ordonnanceur */ ordonnanceur(); /* Récupération du contexte de la tâche ré-activée */ SP = Taches[courant].SPointeur; RESTORE_REGISTER(); asm volatile ( "reti" ); } |
Nous avons décider de faire clignoter une LED (PORTD ^= 0x02) à chaques utilisations de la fonction ordonnanceur() dans l'ISR.
La variable courant indique la Tache qui est actuellement en cours.
A chaque chaques changements de tache, l'ISR se déclenche enregistre la tache en cours (les registres ainsi que le Stack Pointer), appel l'ordonnanceur, puis change "charge" la tache suivante.
On oublie pas maintenant de générer l'interruption toute les 20ms, pour cela on choisit un prescaler de 1024 et un nombre de ticks de 312.
Code |
---|
#define PRESCALER 1024 #define NB_TICK 312 #define CTC1 WGM12 void init_timer(){ TCCR1A = 0; // No output pin connected, no PWM mode enabled TCCR1B = 1<<CTC1; // No input pin used, clear timer counter on compare match #if (PRESCALER==1024) TCCR1B |= (1<<CS12 | 1<<CS10); #endif OCR1A = NB_TICK; TCNT1 = 0; TIMSK1 = (1<<OCIE1A); // No overflow mode enabled, no input interrupt, output compare interrupt } |
Ainsi que d'initialiser les Taches pour assurer le bon fonctionnement de notre ordonnanceur.
Code |
---|
void init_taches(int pid) { int save=SP; SP=Taches[pid].SPointeur; uint16_t adresse=(uint16_t)Taches[pid].depart; asm volatile ("push %0 \n\t" : : "r" (adresse & 0x00ff)); asm volatile ("push %0 \n\t" : : "r" ((adresse & 0xff00) >> 8)); SAVE_REGISTER(); Taches[pid].SPointeur=SP; SP=save; } |
On peut maintenant tester notre ordonnanceur.
Code | Vidéo |
---|---|
#define NBR_TACHE 2 int main(void){ init_ports(); init_timer(); init_taches(1); sei(); SP = Taches[courant].SPointeur; Taches[courant].depart(); return 0; } |
Les 2 leds clignotent bien avec le bon timing, et la led témoin de l'ordonnanceur clignotent aussi, ce qui indique que l'ISR est bien actif.
Nous pouvons donc maitenant passer à la gestion des "sleeps".
En effet, l'utilisation de _delay_ms dans chaque tache n'est pas la manière la plus optimale de procéder. Et meme cela nous bloquera par la suite.
Nous ajoute donc à la structure de nos Taches, une structure InfoEndormi, qui, comme son nom l'indique nous donne des informations si la tache est endormie. A savoir la raison mais aussi des donnees comme par exemple le temps que cette tache doit passer endormie.
Puis afin de remplacer _delay_ms, nous créeons une fonction Attente qui endors une tache pour un temps donné.
Code | Code |
---|---|
#define REVEILLE 0 #define ENDORMI 1 #define RAISON_DELAY 0 struct InfoEndormi{ int raison; uint16_t donnee; }sleep_t; struct TacheInfo{ void (*depart)(void); uint16_t SPointeur; int etat; struct InfoEndormi endormi; }; |
void Attente(uint16_t temps_ms){ cli(); Taches[Tache_courante].etat = ENDORMI; Taches[Tache_courante].endormi.raison = RAISON_DELAY; Taches[Tache_courante].endormi.donnee = temps_ms; TCNT1 = 0; sei(); } |
Voila maitenant nos taches, il est important de créer une TacheZombie qui comme son nom l'indique est toujours révéillée.
Code |
---|
void TacheZombie() //Tache toujours réveillé { while(1) { _delay_ms(1.5*PERIODE_INTERUPTION); //Delay légèrement supèrieur à la periode pour éviter tout soucis } } void TacheA() { while(1) { PORTC ^= 0x08; Attente(300); } } void TacheB() { while(1) { PORTD ^= 0x80; Attente(500); } } |
Il est important maitenant de modifier notre ordonnanceur afin que celui-ci puisse recalculer le nouveau temps de sleep d'une tache et si ce temps devient inférieur ou ègale à 0 alors l'ordonnacnceur réveille la tache.
Code |
---|
void ordonnanceur () { PORTD ^= 0x02; for(int i = 0; i < NBR_TACHE; i++) { if(Taches[i].etat == ENDORMI && Taches[Tache_courante].endormi.raison == RAISON_DELAY) { uint16_t diff_temps = PERIODE_INTERUPTION; if(TCNT1 != 0) { //Calcul précis de la différence de temps (pas obligatoirement 20 ms) diff_temps = ((TCNT1*PERIODE_INTERUPTION*10)/OCR1A)/10; TCNT1 = 0; } Taches[i].endormi.donnee -= diff_temps; if(Taches[i].endormi.donnee <= 0) { Taches[i].etat = REVEILLE; } } } do { Tache_courante++; if (Tache_courante == NBR_TACHE) Tache_courante = 0; } while(Taches[Tache_courante].etat == ENDORMI); } |
Maintenant que cela est fait, nous pouvons ajouter 2 nouvelles taches qui sont la lecture et l'ecriture sur le port série.
Pour cela, nous allons procéder avec des sémaphores. En effet, il ne peut y avoir que 1 seule tache en écriture ou en lecture, il faut donc que si plusieurs taches essaient de prendre la ressource, cette ressource soit donner à la première arrivé et que les autres taches en attendant patientent.
Code |
---|
void semaphore_e(int ACTION){ if (ACTION == PRENDRE) { while(1) { cli(); if (semaphore_ecriture > 0) //Si ressource dispo { semaphore_ecriture = 0; sei(); break; } else { // La ressource est occupée, on endort la tache Taches[Tache_courante].etat = ENDORMI; Taches[Tache_courante].endormi.raison = RAISON_ECRITURE; TCNT1=0; TIMER1_COMPA_vect(); } sei(); while(semaphore_ecriture==0); } } else if (ACTION == LAISSER) { cli(); semaphore_ecriture = 1; sei(); } } |
On fait la meme chose pour la lecture.
On oublie pas d'initialiser le port série avec les bons paramètres, créer les taches, puis on test.
Code | Image |
---|---|
void init_serie(long int vitesse){ UBRR0=FREQ_CPU/(((unsigned long int)vitesse)<<4)-1; // configure la vitesse UCSR0B=(1<<TXEN0 | 1<<RXEN0); // autorise l'envoi et la réception UCSR0C=(1<<UCSZ01 | 1<<UCSZ00); // 8 bits et 1 bit de stop UCSR0A &= ~(1 << U2X0); // double vitesse désactivée } |
Carte électronique numérique
Conception et Création
Nous avons choisi la Carte Fille : Ecran LCD
Pendant la 3ème séance nous avons finis le Schématic de la carte
Puis nous avons Routé la carte et envoyé à la Fabrication
Carte non soudée | Carte soudée | Video Test |
---|---|---|
Programmation de la carte LCD
Tout d'abord, nous avons voulu tester le bon fonctionnement de notre carte mais surtout du bon controle de l'ecran HD44780 grâce à l'IDE Arduino.
Code | Image |
---|---|
const int rs = 2, en = 18, d4 = 14, d5 = 15, d6 = 16, d7 = 17; LiquidCrystal lcd(rs, en, d4, d5, d6, d7); void setup() { // set up the LCD's number of columns and rows: pinMode(9,OUTPUT); digitalWrite(9,LOW); lcd.begin(16, 2); // Print a message to the LCD. lcd.print("Je compte"); } void loop() { // set the cursor to column 0, line 1 // (note: line 1 is the second row, since counting begins with 0): lcd.setCursor(0, 1); // print the number of seconds since reset: lcd.print(millis() / 1000); } |
Maintenant que cela fonctionne, nous pouvons commencer à écrire notre Librairie pour faire fonctionner notre carte.
Librairie HD44780
Nous avons commencer par reprendre la librairie d'ancien élève, merci à eux pour leur travail.
Dans notre cas, pour l'utilisation la plus simple nous avons besoin de plusieurs fonctions. La première écrit un caractère sur l'écran, la deuxième copie une ligne et la dernière efface une ligne.
HD44780_CopyLine1in0 :
Permet de copier la seconde ligne sur la première. Pour ce faire, on se place au début de la première ligne, on écrit le contenu du tableau de caractère, et on le vide ensuite (EmptyLine1).
Code |
---|
void HD44780_CopyLine1in0(char line1[], int nbrows, int nbcols, int ind_line1) { for (int i = 0; i < ind_line1; i++) { int address_ligne0 = HD44780_XY2Adrr(nbrows, nbcols, 0, i); HD44780_WriteCommand(LCD_ADDRSET | address_ligne0); HD44780_WriteData(line1[i],0); } HD44780_EmptyLine1(line1,nbcols); } |
HD44780_EraseLine :
Permet d'effacer une ligne. Pour ce faire on se place au début de la ligne voulue, et on écrit une suite de caractère vide.
Code |
---|
void HD44780_EraseLine(int nbrows, int nbcols, int line) { int address_line1; char espace = ' '; for (int i = 0; i < nbcols; i++) { address_line1 = HD44780_XY2Adrr(nbrows, nbcols, line, i); HD44780_WriteCommand(LCD_ADDRSET | address_line1); HD44780_WriteData(espace,0); } } |
HD44780_WriteChar :
Permet d'écrire un caractère sur l'écran. Chaque caractère écrit sur la seconde ligne est placé dans un tableau de caractère.
Lorsqu'on arrive au bout d'une ligne : Si nous sommes à la première ligne (Ligne 0), on passe à la seconde (Ligne 1) et on se replace au début des colonnes. Si nous sommes à la seconde ligne, on efface les deux lignes et on copie la seconde ligne sur la première ligne de l'écran, et on se replace au début de la seconde.
Code |
---|
void HD44780_WriteChar(char car, char line1[], int* ind_line1, int nbrows, int nbcols, int* row, int* col) { if ((*col) == nbcols) { (*col) = 0; if ((*row) <= 1) (*row)++; if ((*row) > 1) { (*row) = 1; HD44780_EraseLine(nbrows, nbcols,0); HD44780_EraseLine(nbrows, nbcols,1); HD44780_CopyLine1in0(line1, nbrows, nbcols,*ind_line1); } (*ind_line1) = 0; } int address = HD44780_XY2Adrr(nbrows, nbcols, *row, *col); HD44780_WriteCommand(LCD_ADDRSET | address); HD44780_WriteData(car,1); (*col)++; if ((*row) == 1) { line1[*ind_line1] = car; (*ind_line1)++; } } |
VIDEO
Super, ca fonctionne !
Désormais, nous voulons que notre écran affiche ce qu'il recoit via le bus SPI.
Communication SPI
Pour cela il nous faut initialiser le maitre (un arduino UNO) et l'esclave (notre carte LCD).
Puis que le maitre transmette des données et que l'esclave les recoivent.
Code Maitre | Code Esclave |
---|---|
void SPI_init() { DDRB |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS); DDRB &= ~(1 << SPI_MISO); SPCR |= (1 << MSTR); SPCR |= (1 << SPE) | (1<<SPR0) | (1<<SPR1); } void SPI_MasterTransmit(char cData) { SPDR = cData; while(!(SPSR & (1<<SPIF))); } |
void SPI_init() { DDRB |= (1 << SPI_MISO); DDRB &= ~((1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_SS)); SPCR |= (1 << SPE); } char SPI_SlaveReceive(void) { /* Attendre la fin de la réception */ while (!(SPSR & (1 << SPIF))); char receivedData = SPDR; return receivedData; } |
Faisons un petit test en envoyant des 'A' suivis de 'B' et enfin un 'X' pour terminer.
Code Test du Maitre | Video |
---|---|
int main() { ... ... while (x<17) { SPI_MasterTransmit('A'); _delay_ms(100); SPI_MasterTransmit('B'); x++; PORTD ^= (1<<7); _delay_ms(100); } SPI_MasterTransmit('X'); while(1) { } } |
Notre test fonctionne mais cela n'a pas été sans peine. Nous avons rencontré énormément de problème sur la communication SPI. La communication était clairement douteuse et aléatoire.
Problèmes rencontrés
Plusieurs pistes ont été considérées pour comprendre le problème qu'on avait avec le SPI, était-ce l'arduino UNO? Notre connecteur? Le shield? Notre carte? Au départ, par soucis de praticité, nous voulions utiliser l'arduino UNO qui disposait de fonctionnalités de test simple avant de passer au shield, mais à chaque essai, le SPI ne fonctionait pas correctement bien qu'on pouvait observer à l'oscilloscope que le message était bien envoyé à notre carte.
Finalement, nous sommes directement passé à la communication SPI avec notre shield, qui cette fois-ci, permettait d'avoir un résultat satisfaisant. Le résultat n'était pas parfait car on remarquait que la position des fils déterminait si la communication allait fonctionner, il fallait donc trouver la position qui permettait cette communication. Pour régler ce problème, nous avons déterminé la fréquence de travail du SPI utilisé par le shield et notre carte, pour vérifier qu'il n'y ait pas de problèmes de synchronisation ou que la fréquence utilisée soit bien adaptée. Pour ce faire nous avons observé à l'oscilloscope les différentes divisions de fréquences possibles en vérifiant en même temps que la communication SPI fonctionne bien. Les résultats sont surprenant, car peu importe la fréquence de travail, aucun problème n'a été détécté.
Communication SPI par interruption
Il nous faut maitenant communiquer par SPI par interruption car cela offre certains avantages par rapport à la communication SPI standard sans interruption, en particulier en termes d'efficacité et de gestion du temps. En effet, l'utilisation d'interruptions permet au microcontrôleur de gérer d'autres tâches pendant les transferts SPI. Plutôt que d'attendre de manière synchrone la fin d'une transaction SPI, le microcontrôleur peut être informé via une interruption lorsque les données sont prêtes à être lues ou écrites. Cela permet un multitâche plus efficace. Ce qui va nous être trés pratique sachant que le projet comporte plusieurs cartes filles.
Pour cela il nous faut coté maitre et esclave activé le mode SPI Interrupt lors de l'initialisation du SPI, on rajoute donc SPCR |= (1<<SPIE);
lors à l'initialisation.
Ensuite on génère l'interruption coté esclave à la fin de l'initalisation et on configure notre ISR.
Le maitre à plusieurs type d'envoie possible :
0x00 : Demande le type de la carte.
0x01 : Active la communication avec la carte LCD.
0xff : Met fin à la communication.
Voici comment l'ISR lui fonctionne, si je recois 0x00 je renvoie on type, 0x01 je passe en mode EnReception.
En mode EnReception : lorsque je recois un octet je l'ajoute à un buffer, lorsque je recois 0xff je désactive le mode en Reception.
Dans mon main, une fois que mon mode EnRecption est désactivé mais que mon indice du buffer et supérieur à 0 alors je peux traiter mes octets.
Code ISR | Code Main |
---|---|
ISR(SPI_STC_vect) { uint8_t data = SPDR; if(EnReception == 1) switch (data){ case 0xff: EnReception=0; break; default : buffer[IndBuffer++]=data; } else switch (data){ case 0x00: sendType(); break; case 0x01: EnReception=1; break; } } |
while (1) { if (EnReception == 0 && IndBuffer>0) { for(int i=0; i < IndBuffer; i++) { "Fonction de traitement" } IndBuffer=0; } } |
Gestion des caractères spéciaux
Nous avons décider de gérer 4 caractères spéciaux différents : \n , \t, \b, \r
Pour cela on créé la fonction HD44780_Traitement
qui sera notre fonction principale.
Si on recoit un caractère classique alors on l'affiche grâce à HD44780_WriteChar
précédemment crée.
Sinon il sagit d'un caractère spécial et alors on le traite dans un switch case :
\n
Si on est sur la première ligne, on se place au début de la seconde.
Si on est sur la seconde ligne, on efface les deux lignes et on copie la seconde ligne sur la première ligne de l'écran, et on se replace au début de la seconde.
Code \n | Video |
---|---|
case '\n': //Retour à la ligne switch (*row) { case 0: (*col) = 0; (*row) = 1; break; case 1: *col = 0; HD44780_EraseLine(nbrows, nbcols,0); HD44780_EraseLine(nbrows, nbcols,1); HD44780_CopyLine1in0(line1, nbrows, nbcols,*ind_line1); *ind_line1 = 0; break; } break; |
\r
On se replace au début de la ligne sur laquelle on est actuellement en train d'écrire.
Code \r | Video |
---|---|
case '\r': // Retour chariot *col = 0; (*ind_line1) = 0; HD44780_EmptyLine1(line1,nbcols); break; |
\r
On efface le caractère précédent, ajuste la position du curseur, et passe à la ligne supérieure si nécessaire.
Code \b | Video |
---|---|
case '\b': // Retour arrière switch (*row) { case 0: if (*col > 0) { (*col)--;//Retourne sur le caractère précédent char espace = ' '; int address = HD44780_XY2Adrr(nbrows, nbcols, *row, *col); HD44780_WriteCommand(LCD_ADDRSET | address); HD44780_WriteData(espace, 1);//"L'efface" _delay_ms(500); } break; case 1: if (*col > 0) { (*col)--; (*ind_line1) --; } else { (*row) = 0; (*col) = 15; } char espace = ' '; int address = HD44780_XY2Adrr(nbrows, nbcols, *row, *col); HD44780_WriteCommand(LCD_ADDRSET | address); HD44780_WriteData(espace, 1); break; } break; |
\t
On écrit 4 espaces, à la suite de notre position actuelle.
Code \t | Video |
---|---|
case '\t': // Tabulation de 4 caractères for (int i=0; i < 4; i++) { char espace = ' '; HD44780_WriteChar(espace,line1,ind_line1,nbrows,nbcols,row,col); } break; |
Gestion des codes VT100
Pour finir, nous allons gérer quelques codes VT100, les déplacements de curseurs.
Pour envoyer un code VT100 pour déplacer le curseur le maitre doit nous envoyer:
- Le code ESCAPE (0x1b)
- Le CURSOR_MODE (0x1c)
- Et enfin le mouvement du curseur souhaité CURSOR_UP (0x41), CURSOR_DOWN (0x42), CURSOR_RIGHT (0x43), CURSOR_LEFT (0x44).
On gère cela dans la fonction VT100_Traitement.
Code |
---|
void VT100_Traitement(char car, int nbrows, int nbcols, int* row, int* col, int* vt100_mode) { if (*vt100_mode == 1) // CURSOR_MODE { int address; switch (car) { case CURSOR_UP: *row = (*row == 1) ? 0 : *row; break; case CURSOR_DOWN: *row = (*row == 0) ? 1 : *row; break; case CURSOR_RIGHT: *col = (*col < nbcols - 1) ? (*col + 1) : *col; break; case CURSOR_LEFT: *col = (*col > 0) ? (*col - 1) : *col; break; } address = HD44780_XY2Adrr(nbrows, nbcols, *row, *col); HD44780_WriteCommand(LCD_ADDRSET | address); HD44780_WriteCommand(LCD_ON | CURSOR_BLINK); } } |
VIDEO
Gestion des primitives systèmes
Voici les primitives systèmes pour la carte mère, elle permettent de communiquer les chaînes de caractères à envoyer en préparant une transmission SPI constituée de l'octet de commande 0x01 et des données correspondants aux caractères.
La fonction Reset_Display permet de reset l'affichage de l'écran et permet aussi à la carte mère de le remarquer en lui attribuant un numéro de port.
La fonction Transmit_to_display est la fonction de transmission de données par SPI. Elle utilise les fonctions SPI créées précédemment pour envoyer les données à afficher.
Code |
---|
void Reset_display(volatile uint8_t *resPort, volatile uint8_t resSel) //Utile pour reset l'ecran { *resPort &= ~(1<<resSel); _delay_ms(1); *resPort |= (1<<resSel); } void Transmit_To_display(uint8_t data, volatile uint8_t *ssPort, volatile uint8_t ss) { selectSlaveSPI(ssPort,ss); _delay_ms(1); transferSPI(data); _delay_ms(1); unselectSlaveSPI(ssPort,ss); _delay_ms(1); } |
Conclusion
Références utiles
Lien du git :
https://archives.plil.fr/mchauvel/PICO_Taha_NEHARI_Martin_CHAUVELIERE.git