« SE4Binome2025-3 » : différence entre les versions
m (→Main) |
m (→Tests du code) |
||
| (27 versions intermédiaires par le même utilisateur non affichées) | |||
| Ligne 20 : | Ligne 20 : | ||
Notre carte ressemblant à | Notre carte ressemblant à celle du [https://projets-se.plil.fr/mediawiki/index.php/SE4Binome2025-1 binôme 1], nous utiliserons par conséquent la leur. | ||
Après réception, on a premièrement testé le bon fonctionnement de la carte via un code sommaire pour allumer et éteindre les leds sans réelles conditions. | |||
== Partie Carte Clavier == | == Partie Carte Clavier == | ||
=== Mise en place === | === Mise en place === | ||
Nous avons d'abord eu à définir le projet pour le clavier et nous avons décidé de réaliser entièrement notre carte clavier de manière matricielle. Nous avons choisi de créer un clavier de 30 touches permettant d'écrire 26 caractères par mode avec 3 modes différents. Les 4 touches restantes serviront de touches Supprimer, Espace, Maj et Symbole. Ces touches seront communes aux 3 modes du clavier. Nous allons profiter du fait d'avoir attribué une diode à chaque touche pour pouvoir en presser deux en même temps. Comme le fonctionnement de la touche Shift, il faut garder la touche enfoncée pour changer de mode. MAJ enfoncée pour les majuscules et SYMBOLE pour les chiffres et symboles. En appuyant sur MAJ et SYMBOLE simultanément on pourra activer la touche Entrée. | Nous avons d'abord eu à définir le projet pour le clavier et nous avons décidé de réaliser entièrement notre carte clavier de manière matricielle. Il nous semblait plus intéressant de concevoir la carte depuis "rien" pour pouvoir pratiquer de tout, que ce soit la réalisation du PCB jusqu'au code. | ||
Nous avons choisi de créer un clavier de 30 touches permettant d'écrire 26 caractères par mode avec 3 modes différents. Les 4 touches restantes serviront de touches Supprimer, Espace, Maj et Symbole. Ces touches seront communes aux 3 modes du clavier. Nous allons profiter du fait d'avoir attribué une diode à chaque touche pour pouvoir utiliser l'anti-ghosting et en presser deux en même temps. Comme le fonctionnement de la touche Shift sur un clavier ordinaire, il faut garder la touche enfoncée pour changer de mode. | |||
MAJ enfoncée pour les majuscules et SYMBOLE pour les chiffres et symboles. En appuyant sur MAJ et SYMBOLE simultanément on pourra activer la touche Entrée. | |||
=== Disposition des touches === | === Disposition des touches === | ||
| Ligne 149 : | Ligne 153 : | ||
Les lignes et colonnes de touches sont donc reliées aux broches des ports C et D et chacun des boutons est relié à une diode, ce qui nous permettra de taper sur plusieurs touches en même temps, nous permettant de changer les modes de fonctionnement du clavier entre minuscule, majuscule et symbole. | Les lignes et colonnes de touches sont donc reliées aux broches des ports C et D et chacun des boutons est relié à une diode, ce qui nous permettra de taper sur plusieurs touches en même temps, nous permettant de changer les modes de fonctionnement du clavier entre minuscule, majuscule et symbole. | ||
On place également des résistances de pull up sur les colonnes pour stabiliser la détection et lire l'état bas plutôt que l'état haut. | |||
Pour s'assurer d'être dans le bon mode de clavier, nous avons placé 3 LEDs qui indiquent chacune un mode différent. | Pour s'assurer d'être dans le bon mode de clavier, nous avons placé 3 LEDs qui indiquent chacune un mode différent. | ||
| Ligne 171 : | Ligne 177 : | ||
Après réception de la carte, une erreur a été remarquée. Sur le schématique comme sur le routage, les empreintes +5V et +VCC ne sont pas censées être distinctes. Pour pallier cette erreur on soude un fil entre les résistances R8 et R5 pour fusionner VCC et +5V. | |||
=== Code clavier === | === Code clavier === | ||
Le code C du clavier peut être trouvé sur le [https://gitea.plil.fr/mgourves/SE4-PICO-B3/src/branch/main/codeClavier/clavier.c git]. | Le code C du clavier peut être trouvé sur le [https://gitea.plil.fr/mgourves/SE4-PICO-B3/src/branch/main/codeClavier/clavier.c git]. | ||
==== Variables et constantes ==== | ==== Variables et constantes ==== | ||
Tout d'abord nous avons initialisé les différentes librairies et les constantes globales utiles au programme. | |||
Nous avons donc 6 colonnes et 5 lignes, on retient aussi les "coordonnées" des touches MAJ et SYMBOLE. | |||
La constante d'anti rebond permet de ralentir le programme pour éviter que plusieurs caractères soient envoyés avec un seul appui.<syntaxhighlight lang="c"> | |||
#define NB_COL 6 | #define NB_COL 6 | ||
#define NB_ROW 5 | #define NB_ROW 5 | ||
| Ligne 198 : | Ligne 203 : | ||
#define MAX_PRESSED_KEYS 2 | #define MAX_PRESSED_KEYS 2 | ||
#define ANTI_REBOND_mS 10 //10ms pour éviter le "spam" d'envoi | #define ANTI_REBOND_mS 10 //10ms pour éviter le "spam" d'envoi | ||
</syntaxhighlight>Ensuite, nous avons déclaré les variables globales liées aux ports utilisés par les touches du clavier. On | </syntaxhighlight>Ensuite, nous avons déclaré les variables globales liées aux ports utilisés par les touches du clavier. On mappe les colonnes et les lignes avec les pins correspondants.<syntaxhighlight lang="c" start="18"> | ||
uint8_t BIT_Row[NB_ROW] = {PD1,PD0,PC5,PC4,PC3}; | uint8_t BIT_Row[NB_ROW] = {PD1,PD0,PC5,PC4,PC3}; | ||
uint8_t BIT_Col[NB_COL] = { | uint8_t BIT_Col[NB_COL] = {/*...*/}; | ||
volatile uint8_t *DDR_Row[NB_ROW] = {&DDRD,&DDRD,&DDRC,&DDRC,&DDRC}; | volatile uint8_t *DDR_Row[NB_ROW] = {&DDRD,&DDRD,&DDRC,&DDRC,&DDRC}; | ||
volatile uint8_t *DDR_Col[NB_COL] = { | volatile uint8_t *DDR_Col[NB_COL] = {/*...*/}; | ||
volatile uint8_t *PORT_Row[NB_ROW] = {&PORTD, | volatile uint8_t *PORT_Row[NB_ROW] = {&PORTD,/*...*/}; | ||
volatile uint8_t *PORT_Col[NB_COL] = { | volatile uint8_t *PORT_Col[NB_COL] = {/*...*/}; | ||
volatile uint8_t *PIN_Col[NB_COL] = {&PINC,&PINC,&PINC,&PIND,&PIND,&PIND}; | volatile uint8_t *PIN_Col[NB_COL] = {&PINC,&PINC,&PINC,&PIND,&PIND,&PIND}; | ||
</syntaxhighlight>Nous avons également déclaré une matrice 3D représentant notre clavier avec les codes | </syntaxhighlight>Nous avons également déclaré une matrice 3D représentant notre clavier avec les codes ASCII décimaux correspondant à chaque touche. | ||
Les constantes MAJ et SYMBOLE étant déclarées avec des valeurs dépassant la table ASCII basique pour éviter les conflits mais tout de même avoir une valeur enregistrée. <syntaxhighlight lang="c" start="28"> | |||
int bind[NB_MODE][NB_ROW][NB_COL]={ //ASCII décimal | int bind[NB_MODE][NB_ROW][NB_COL]={ //ASCII décimal | ||
{ {97,98,99,100,101,102}, | { {97,98,99,100,101,102}, | ||
| Ligne 217 : | Ligne 224 : | ||
{121,122,127,32,MAJ,SYMBOLE} } , //mode défaut | {121,122,127,32,MAJ,SYMBOLE} } , //mode défaut | ||
{ { | { {...} } , // Maj | ||
{ {...} } ; //Symbole | |||
{ { | |||
</syntaxhighlight> | </syntaxhighlight> | ||
==== Fonctions ==== | ==== Fonctions ==== | ||
Les ports doivent êtres initialisés, les ports correspondant aux colonnes seront en entrée pour détecter un appui et les lignes en sortie. | Les ports doivent êtres initialisés, les ports correspondant aux colonnes seront en entrée pour détecter un appui et les lignes en sortie pour pouvoir les activer et désactiver pour la lecture au cas par cas. | ||
Pour le SPI, on le met en maître et on assigne MOSI,SCK et SS en sortie.<syntaxhighlight lang="c | Pour le SPI, on le met en maître et on assigne MOSI,SCK et SS en sortie.<syntaxhighlight lang="c" start="46"> | ||
void initIn(void){ | void initIn(void){ | ||
for(int row = 0; row < NB_ROW ; row++){ | for(int row = 0; row < NB_ROW ; row++){ | ||
| Ligne 250 : | Ligne 248 : | ||
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0); | SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0); | ||
} | } | ||
</syntaxhighlight>Notre fonction principale permettant le scan des touches parcourt d'abord chaque ligne en l'activant puis en parcourant chaque colonne pour vérifier si un appui est réalisé. Si on dépasse le nombre de touches | </syntaxhighlight>Notre fonction principale permettant le scan des touches parcourt d'abord chaque ligne en l'activant puis en parcourant chaque colonne pour vérifier si un appui est réalisé. | ||
On y ajoute un délai pour laisser le temps de scanner la touche. | |||
Si on dépasse le nombre de touches appuyées elle s'arrête sinon elle sauvegarde la ligne et la colonne correspondantes à la touche pressée dans le tableau pressed_keys[][] déclaré globalement. | |||
Après le parcours de toute la ligne cette dernière est désactivée. <syntaxhighlight lang="c" start="62"> | |||
void scanTouche(void){ | void scanTouche(void){ | ||
counter_pressed = 0; | counter_pressed = 0; | ||
for(uint8_t row = 0 ; row < NB_ROW; row++ ){ | for(uint8_t row = 0 ; row < NB_ROW; row++ ){ | ||
*(PORT_Row[row]) &= ~(1 << BIT_Row[row]); | *(PORT_Row[row]) &= ~(1 << BIT_Row[row]); | ||
_delay_us(1); | |||
for(uint8_t col = 0; col < NB_COL; col++){ | for(uint8_t col = 0; col < NB_COL; col++){ | ||
if(counter_pressed > MAX_PRESSED_KEYS-1){return;} | if(counter_pressed > MAX_PRESSED_KEYS-1){return;} | ||
| Ligne 270 : | Ligne 275 : | ||
==== Main ==== | ==== Main ==== | ||
On initialise les variables utiles au programme.<syntaxhighlight lang="c"> | On initialise les variables utiles au programme. | ||
* valeur_touche est la valeur ASCII de la touche, à 0 initialement pour le caractère NULL. | |||
* row_touche est l'index de la ligne, à -1 initialement pour mettre une condition bloquante à l'envoi si seule une touche MAJ ou SYMBOLE est pressée. | |||
* mode est à 0 car le mode minuscule est le mode par défaut. | |||
<syntaxhighlight lang="c"> | |||
int valeur_touche = 0; | int valeur_touche = 0; | ||
int row_touche = -1; | int row_touche = -1; | ||
| Ligne 277 : | Ligne 287 : | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==== Boucle while ==== | |||
On | |||
On utilise d'abord notre fonction pour scanner la touche pressée et on place ses coordonnées dans des variables r et c plus claires.<syntaxhighlight lang="c"> | |||
while(1){ | while(1){ | ||
scanTouche(); | scanTouche(); | ||
| Ligne 286 : | Ligne 297 : | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==== Analyse de la touche ==== | |||
Premièrement on vérifie si la touche est SYMBOLE ou MAJ | |||
Premièrement on vérifie si la touche est SYMBOLE ou MAJ avec les coordonées déclarées. Si c'est le cas on vérifie que l'autre touche spéciale ne soit pas déjà pressée en vérifiant que le mode soit égal à 0, si ce n'est pas le cas on envoie la touche Entrée. Sinon on change le mode pour la valeur correspondante. <syntaxhighlight lang="c"> | |||
if(r == MAJ_ROW && c == MAJ_COL ){ | if(r == MAJ_ROW && c == MAJ_COL ){ | ||
if(mode == 2) { | if(mode == 2) { | ||
| Ligne 296 : | Ligne 308 : | ||
mode = 1; | mode = 1; | ||
} | } | ||
else if(r == SYMBOLE_ROW && c == SYMBOLE_COL ){ | |||
if(mode == 1) { | if(mode == 1) { | ||
valeur_touche = ASCII_ENTER; | valeur_touche = ASCII_ENTER; | ||
| Ligne 304 : | Ligne 316 : | ||
mode = 2; | mode = 2; | ||
} | } | ||
</syntaxhighlight>Dans le cas d'une touche "normale" on retient les coordonnées, et on envoie via SPI le caractère correspondant si jamais une touche est pressée. | </syntaxhighlight>Dans le cas d'une touche "normale" on retient les coordonnées, et on envoie via SPI le caractère correspondant à ces coordonnées dans la matrice si jamais une touche est pressée. On rajoute un délai pour éviter le rebond lors d'un appui.<syntaxhighlight lang="c"> | ||
else { | else { | ||
row_touche = r; | row_touche = r; | ||
col_touche = c; | col_touche = c; | ||
} | } | ||
if(counter_pressed > 0 && row_touche > -1 && col_touche > -1){ | if(counter_pressed > 0 && row_touche > -1 && col_touche > -1){ | ||
valeur_touche = bind[mode][row_touche][col_touche]; | valeur_touche = bind[mode][row_touche][col_touche]; | ||
| Ligne 315 : | Ligne 326 : | ||
} | } | ||
if (valeur_touche!=0) {_delay_ms(ANTI_REBOND_mS);} | if (valeur_touche!=0) {_delay_ms(ANTI_REBOND_mS);} | ||
</syntaxhighlight>On finit ensuite par reset les valeurs des variables pour | </syntaxhighlight>On finit ensuite par reset les valeurs des variables pour recommencer le scan.<syntaxhighlight lang="c"> | ||
mode = 0; | mode = 0; | ||
row_touche = -1; | row_touche = -1; | ||
| Ligne 321 : | Ligne 332 : | ||
valeur_touche = 0; | valeur_touche = 0; | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==== Tests du code ==== | |||
Lors de l'implémentation du code dans le clavier, on remarque le bon fonctionnement des leds d'indication des différents mode, cependant pour pouvoir lire les caractères tapés pour vérifier le fonctionnement, on ne peut pas lire directement via le minicom. Il faut que l'arduino les lise puis les envoie lui même. Pour cela on doit téléverser un code sur l'arduino qui sert de "relai" entre l'avr et le PC. | |||
Interruption déclenchée à chaque réception SPI, elle conserve la data reçue dans une variable globale ''received'' et passe le ''flag'' à 1 pour signaler la réception de data.<syntaxhighlight lang="c"> | |||
ISR(SPI_STC_vect) { | |||
received = SPDR; // lecture du SPI | |||
flag = 1; | |||
} | |||
</syntaxhighlight>On calcule le baudrate ''ubrr'' (ici 9600) et on active l’émetteur UART avec ''UCSR0B.'' | |||
Pour le SPI, on mets le MISO en sortie et on active le SPI et l'interruption SPI avec ''SPE'' et ''SPIE.''<syntaxhighlight lang="c"> | |||
void uart_init(void) { | |||
uint16_t ubrr = F_CPU / 16 / 9600 - 1; | |||
UBRR0H = (ubrr >> 8); | |||
UBRR0L = ubrr; | |||
UCSR0B = (1 << TXEN0); | |||
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00); | |||
} | |||
void spi_init_slave(void) { | |||
DDRB |= (1 << PB4); | |||
SPCR = (1 << SPE) | (1 << SPIE); // SPI + interruption | |||
} | |||
</syntaxhighlight>On attend que le registre d'envoi soit vide puis on écrit le caractère.<syntaxhighlight lang="c"> | |||
void uart_send(char c) { | |||
while (!(UCSR0A & (1 << UDRE0))); | |||
UDR0 = c; | |||
} | |||
</syntaxhighlight>On initialise le système puis on rentre dans une boucle infinie, si un flag est détécté, on envoie la data reçue avec ''uart_send()'' puis on réinitialise le flag.<syntaxhighlight lang="c"> | |||
int main(void) { | |||
uart_init(); | |||
spi_init_slave(); | |||
sei(); // activation interruptions | |||
while (1) { | |||
if (flag) { | |||
uart_send(received); | |||
flag = 0; | |||
} | |||
} | |||
} | |||
</syntaxhighlight>On peut désormais lire les caractères sur le minicom. | |||
=== Ordonnanceur === | |||
Le code complet de l'ordonnanceur est trouvable ici sur le [https://gitea.plil.fr/mgourves/SE4-PICO-B3/src/branch/main/ordo/ordonnanceur.c git]. Les définitions sont trouvable dans le [https://gitea.plil.fr/mgourves/SE4-PICO-B3/src/branch/main/ordo/ordonnanceur.h .h] associé. | |||
On commence par créer une structure qui représente une tâche, elle contient : | |||
* Un pointeur de la fonction correspondante | |||
* Sa place dans la pile pour permettre de la retrouver | |||
* Son état soit endormi soit reveillé | |||
* Un temps de sommeil | |||
<syntaxhighlight lang="c"> | |||
typedef struct{ | |||
void (*addr_fonction)(void); | |||
uint16_t stackPointer; // Pointeur de pile | |||
bool state; // État (actif vide ou finie) | |||
int sleepTime; | |||
} Process; | |||
</syntaxhighlight>La tâche principale consiste à faire clignoter une led et de donner un temps de sommeil.<syntaxhighlight lang="c"> | |||
void led1(){ | |||
while (1){ | |||
PORTC ^= 1; | |||
sleep(287); | |||
} | |||
} | |||
//[led2,led3...] | |||
</syntaxhighlight>On ajoute également une fonction idle, pour qu'il y ai toujours une tâche réveillée si les autres sont endormies, cela empêche un blocage dans une boucle infinie. Cette dernière n'est pas nécessaire dans la version la plus archaïque de l'ordonnanceur mais devient indispensable à partir du moment où on utilise notre fonction sleep().<syntaxhighlight lang="c"> | |||
void anti_block(){ //tache vide pour éviter de rester bloqué si toutes les taches sont endormies | |||
while(1); | |||
} | |||
</syntaxhighlight>La fonction sleep() permet de remettre le timer de l'ISR à (F_CPU/1000*periode/diviseur - 1) et ensuite changer le temps de sommeil de la tâche actuelle par un temps donné.<syntaxhighlight lang="c"> | |||
void sleep(int ms_sleep){ | |||
cli(); | |||
TCNT1=max_counter-1; | |||
liste_process[process_actuel].sleepTime = ms_sleep; | |||
sei(); | |||
} | |||
</syntaxhighlight>La fonction change_process() commence par décrémenter les temps de sommeil de toutes les tâches en liste ou mettre ce temps à 0 si il est inférieur au paramètre de décrémentation. Ensuite il parcourt toutes les tâches réveillées et avec un temps de sommeil supérieur à 0. L'utilisation de la boucle do while au lieu de while permet également de ne pas rester bloquer car le programme exécute d'abord la boucle avant de tester les conditions ce qui permet de toujours parcourir les process même si aucun d'entre eux ne respectent les conditions. Dans le cas d'un simple while, si le process ne respecte pas les conditions, on ne rentrera jamais dans la boucle, puisque on ne change jamais de process.<syntaxhighlight lang="c"> | |||
void change_process(){ | |||
for (int i=0 ; i<NB_PROCESS ; i++){ | |||
if (liste_process[i].sleepTime - S_DECR< 0){ | |||
liste_process[i].sleepTime = 0; | |||
} | |||
else{ | |||
liste_process[i].sleepTime -= S_DECR; | |||
} | |||
} | |||
do{ | |||
process_actuel++; | |||
if (process_actuel >= NB_PROCESS){ | |||
process_actuel = 0; | |||
} | |||
} | |||
while (liste_process[process_actuel].sleepTime > 0 || !(liste_process[process_actuel].state)); | |||
} | |||
</syntaxhighlight>La fonction ajout_tache() permet d'ajouter dynamiquement une tâche à la liste. | |||
On désactive les interruptions puis on parcourt la liste pour trouver la première tâche endormie. | |||
A cette place on réveille la tâche, on y place le pointeur de fonction passée en paramètre et on met à jour le stack pointer et la pile. | |||
On réactive finalement les interruptions.<syntaxhighlight lang="c"> | |||
void ajout_tache(void (*fonction)(void)){ | |||
cli(); | |||
int counter = 0; | |||
while(liste_process[counter].state){ | |||
counter++; | |||
} | |||
liste_process[counter].state = true; | |||
liste_process[counter].addr_fonction = fonction; | |||
init_stackPointer_tasks(counter); | |||
InitialisationPile(counter); | |||
sei(); | |||
} | |||
</syntaxhighlight>D'une manière légèrement symétrique, la fonction tue_tache() retire dynamiquement une tâche de la liste. | |||
On désactive les interruptions puis on parcourt la liste pour trouver la tâche avec la fonction correspondante à celle donnée en paramètre. | |||
Si on la trouve, on endort la tâche et on remplace le pointeur de fonction par le pointeur NULL | |||
On réactive finalement les interruptions.<syntaxhighlight lang="c"> | |||
void tue_tache(void (*fonction)(void)){ | |||
cli(); | |||
for(int counter = 0; counter<NB_PROCESS; counter++){ | |||
if(liste_process[counter].addr_fonction == fonction){ | |||
liste_process[counter].addr_fonction = NULL; | |||
liste_process[counter].state = false; | |||
liste_process[counter].sleepTime = 0; | |||
} | |||
} | |||
sei(); | |||
} | |||
</syntaxhighlight>Pour l'initialisation, on ajoute les tâches une par une avec notre fonction. | |||
On impose les ports des leds à être en sortie. | |||
On appelle les différentes fonctions d'initialisation notamment de stack pointer, de pile,du minuteur pour les interruptions, du SPI et d'état. | |||
Cette dernière parcourt le nombre de tâches possible et les mets vraies si il y a un pointeur de fonction.<syntaxhighlight lang="c"> | |||
void initialisation(){ | |||
//setup fonctions | |||
ajout_tache(led1); | |||
//...leds | |||
ajout_tache(anti_block); | |||
//config ports | |||
DDRC |= 0b00001001; | |||
DDRD |= 0b10010000; | |||
DDRB |= 0b00000001; | |||
init_minuteur(DIVISEUR,PERIODE); | |||
init_etat(); | |||
serial_init(SPEED); | |||
for(int current_process = 0 ; current_process < NB_PROCESS ; current_process ++){ | |||
init_stackPointer_tasks(current_process); | |||
InitialisationPile(current_process); | |||
} | |||
} | |||
void init_etat(){ | |||
for(int i = 0; i < NB_PROCESS; i ++){ | |||
liste_process[i].state = (liste_process[i].addr_fonction != NULL); | |||
} | |||
} | |||
</syntaxhighlight>En guise d'amélioration, on créer une fonction capable de faire choisir via le clavier, quel tâche on endort ou on réveille, ici quelle led fait clignoter ou non. Dans le cas d'un caractère autre que prévu, toutes les tâche sont endormie et, les leds clignotent brièvement pour l'indiquer. | |||
Ici on réveille les process des leds avec les touches 0 à 4 du pavé numérique, et on les désactive avec les touches 5 à 9. Toute autre touche fera clignoter les led un court laps de temps puis les éteindra et tuera les process.<syntaxhighlight lang="c"> | |||
void choix_led(){ | |||
char carac; | |||
while(1){ | |||
carac = serial_get(); | |||
switch (carac){ | |||
case '0': | |||
ajout_tache(led1); | |||
break; | |||
//...pour les ajouts | |||
case '5': | |||
tue_tache(led1); | |||
break; | |||
//...pour les tuer | |||
default: | |||
tue_tache(led1); | |||
//...tue leds | |||
for(int i=0; i<10;i++){/*Clignotement*/} | |||
break; | |||
} | |||
} | |||
} | |||
</syntaxhighlight> | |||
==== Vidéos test ordonnanceur ==== | |||
[[Fichier:Vidéo test.mp4|vignette|gauche|La première version de l'ordonnanceur dans laquelle il n'y avait que des _delay_ms().]] | |||
[[Fichier:Test2ordo.mp4|vignette|centré|La version finale de l'ordonnanceur, dans celle ci on peut controller les processus des 5 leds via le pavé numérique. 0-4 pour réveiller les tâches et 5-9 pour les tuer.]] | |||
Version actuelle datée du 23 janvier 2026 à 17:20
Présentation projet :
Dans ce projet Pico, l'objectif est de réaliser un pico ordinateur composé de différentes cartes fille. La conception des cartes est répartie entre les binômes et dans notre cas, nous nous occuperons de la carte fille clavier.
Pour suivre l'avancée du travail, les fichiers seront déposés sur notre archive git.
Partie Shield
Nous avons tout d'abord commencé par le routage d'un shield Arduino en guise de carte de test pour les cartes filles, dans le cas où la carte mère ne serait pas opérationnelle.
Schématique
En utilisant un modèle donné nous avons fait cette schématique pour notre shield.
Routage
Nous avons routé le shield comme ceci
Notre carte ressemblant à celle du binôme 1, nous utiliserons par conséquent la leur.
Après réception, on a premièrement testé le bon fonctionnement de la carte via un code sommaire pour allumer et éteindre les leds sans réelles conditions.
Partie Carte Clavier
Mise en place
Nous avons d'abord eu à définir le projet pour le clavier et nous avons décidé de réaliser entièrement notre carte clavier de manière matricielle. Il nous semblait plus intéressant de concevoir la carte depuis "rien" pour pouvoir pratiquer de tout, que ce soit la réalisation du PCB jusqu'au code.
Nous avons choisi de créer un clavier de 30 touches permettant d'écrire 26 caractères par mode avec 3 modes différents. Les 4 touches restantes serviront de touches Supprimer, Espace, Maj et Symbole. Ces touches seront communes aux 3 modes du clavier. Nous allons profiter du fait d'avoir attribué une diode à chaque touche pour pouvoir utiliser l'anti-ghosting et en presser deux en même temps. Comme le fonctionnement de la touche Shift sur un clavier ordinaire, il faut garder la touche enfoncée pour changer de mode.
MAJ enfoncée pour les majuscules et SYMBOLE pour les chiffres et symboles. En appuyant sur MAJ et SYMBOLE simultanément on pourra activer la touche Entrée.
Disposition des touches
La disposition est trouvable dans le fichier bind_clavier trouvable sur le git.
| a | b | c | d | e | f |
| g | h | i | j | k | i |
| m | n | o | p | q | r |
| s | t | u | v | w | x |
| y | z | del | space | maj | symbole |
| A | B | C | D | E | F |
| G | H | I | J | K | L |
| M | N | O | P | Q | R |
| S | T | U | V | W | X |
| Y | Z | del | space | maj | symbole |
| 1 | 2 | 3 | & | ; | , |
| 4 | 5 | 6 | " | ? | ! |
| 7 | 8 | 9 | ' | = | + |
| / | 0 | : | ( | ) | _ |
| * | - | del | space | maj | symbole |
Kicad
Nous avons donc mis en place cette matrice sur notre carte KiCad à l'aide d'un atmega328 programmable par un ISP.
Les lignes et colonnes de touches sont donc reliées aux broches des ports C et D et chacun des boutons est relié à une diode, ce qui nous permettra de taper sur plusieurs touches en même temps, nous permettant de changer les modes de fonctionnement du clavier entre minuscule, majuscule et symbole.
On place également des résistances de pull up sur les colonnes pour stabiliser la détection et lire l'état bas plutôt que l'état haut.
Pour s'assurer d'être dans le bon mode de clavier, nous avons placé 3 LEDs qui indiquent chacune un mode différent.
Notre carte sera relié à la carte mère via un connecteur 1*8 placé à l'extremité supérieure de la carte.
Notre carte, une fois routée, ressemble donc à ceci :
Images plus en détails de la carte :
Après réception de la carte, une erreur a été remarquée. Sur le schématique comme sur le routage, les empreintes +5V et +VCC ne sont pas censées être distinctes. Pour pallier cette erreur on soude un fil entre les résistances R8 et R5 pour fusionner VCC et +5V.
Code clavier
Le code C du clavier peut être trouvé sur le git.
Variables et constantes
Tout d'abord nous avons initialisé les différentes librairies et les constantes globales utiles au programme.
Nous avons donc 6 colonnes et 5 lignes, on retient aussi les "coordonnées" des touches MAJ et SYMBOLE.
La constante d'anti rebond permet de ralentir le programme pour éviter que plusieurs caractères soient envoyés avec un seul appui.
#define NB_COL 6
#define NB_ROW 5
#define NB_MODE 3
#define ASCII_ENTER 13
#define MAJ 128
#define MAJ_ROW 4
#define MAJ_COL 4
#define SYMBOLE 129 //pour dépasser la table ascii "normale"
#define SYMBOLE_ROW 4
#define SYMBOLE_COL 5
#define MAX_PRESSED_KEYS 2
#define ANTI_REBOND_mS 10 //10ms pour éviter le "spam" d'envoi
Ensuite, nous avons déclaré les variables globales liées aux ports utilisés par les touches du clavier. On mappe les colonnes et les lignes avec les pins correspondants.
uint8_t BIT_Row[NB_ROW] = {PD1,PD0,PC5,PC4,PC3};
uint8_t BIT_Col[NB_COL] = {/*...*/};
volatile uint8_t *DDR_Row[NB_ROW] = {&DDRD,&DDRD,&DDRC,&DDRC,&DDRC};
volatile uint8_t *DDR_Col[NB_COL] = {/*...*/};
volatile uint8_t *PORT_Row[NB_ROW] = {&PORTD,/*...*/};
volatile uint8_t *PORT_Col[NB_COL] = {/*...*/};
volatile uint8_t *PIN_Col[NB_COL] = {&PINC,&PINC,&PINC,&PIND,&PIND,&PIND};
Nous avons également déclaré une matrice 3D représentant notre clavier avec les codes ASCII décimaux correspondant à chaque touche. Les constantes MAJ et SYMBOLE étant déclarées avec des valeurs dépassant la table ASCII basique pour éviter les conflits mais tout de même avoir une valeur enregistrée.
int bind[NB_MODE][NB_ROW][NB_COL]={ //ASCII décimal
{ {97,98,99,100,101,102},
{103,104,105,106,107,108},
{109,110,111,112,113,114},
{115,116,117,118,119,120},
{121,122,127,32,MAJ,SYMBOLE} } , //mode défaut
{ {...} } , // Maj
{ {...} } ; //Symbole
Fonctions
Les ports doivent êtres initialisés, les ports correspondant aux colonnes seront en entrée pour détecter un appui et les lignes en sortie pour pouvoir les activer et désactiver pour la lecture au cas par cas.
Pour le SPI, on le met en maître et on assigne MOSI,SCK et SS en sortie.
void initIn(void){
for(int row = 0; row < NB_ROW ; row++){
*(DDR_Row[row]) |= (1 << BIT_Row[row]);
}
for(int col = 0; col < NB_COL ; col++){
*(DDR_Col[col]) &= ~(1 << BIT_Col[col]);
*(PORT_Col[col]) |= (1 << BIT_Col[col]);
}
}
[...]
void initSPI(void) {
DDRB |= (1 << PB2) | (1 << PB3) | (1 << PB5);
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
}
Notre fonction principale permettant le scan des touches parcourt d'abord chaque ligne en l'activant puis en parcourant chaque colonne pour vérifier si un appui est réalisé.
On y ajoute un délai pour laisser le temps de scanner la touche.
Si on dépasse le nombre de touches appuyées elle s'arrête sinon elle sauvegarde la ligne et la colonne correspondantes à la touche pressée dans le tableau pressed_keys[][] déclaré globalement.
Après le parcours de toute la ligne cette dernière est désactivée.
void scanTouche(void){
counter_pressed = 0;
for(uint8_t row = 0 ; row < NB_ROW; row++ ){
*(PORT_Row[row]) &= ~(1 << BIT_Row[row]);
_delay_us(1);
for(uint8_t col = 0; col < NB_COL; col++){
if(counter_pressed > MAX_PRESSED_KEYS-1){return;}
if(!(*(PIN_Col[col])&(1 << BIT_Col[col]))){
pressed_keys[counter_pressed][0] = row;
pressed_keys[counter_pressed][1] = col;
counter_pressed++;
}
}
*(PORT_Row[row]) |= (1 <<(BIT_Row[row]));
}
}
Main
On initialise les variables utiles au programme.
- valeur_touche est la valeur ASCII de la touche, à 0 initialement pour le caractère NULL.
- row_touche est l'index de la ligne, à -1 initialement pour mettre une condition bloquante à l'envoi si seule une touche MAJ ou SYMBOLE est pressée.
- mode est à 0 car le mode minuscule est le mode par défaut.
int valeur_touche = 0;
int row_touche = -1;
int col_touche = -1;
int mode = 0;
Boucle while
On utilise d'abord notre fonction pour scanner la touche pressée et on place ses coordonnées dans des variables r et c plus claires.
while(1){
scanTouche();
for(int i = 0;i < counter_pressed ; i++){
int r=pressed_keys[i][0];
int c=pressed_keys[i][1];
Analyse de la touche
Premièrement on vérifie si la touche est SYMBOLE ou MAJ avec les coordonées déclarées. Si c'est le cas on vérifie que l'autre touche spéciale ne soit pas déjà pressée en vérifiant que le mode soit égal à 0, si ce n'est pas le cas on envoie la touche Entrée. Sinon on change le mode pour la valeur correspondante.
if(r == MAJ_ROW && c == MAJ_COL ){
if(mode == 2) {
valeur_touche = ASCII_ENTER;
sendSPI(valeur_touche);
break;
}
mode = 1;
}
else if(r == SYMBOLE_ROW && c == SYMBOLE_COL ){
if(mode == 1) {
valeur_touche = ASCII_ENTER;
sendSPI(valeur_touche);
break;
}
mode = 2;
}
Dans le cas d'une touche "normale" on retient les coordonnées, et on envoie via SPI le caractère correspondant à ces coordonnées dans la matrice si jamais une touche est pressée. On rajoute un délai pour éviter le rebond lors d'un appui.
else {
row_touche = r;
col_touche = c;
}
if(counter_pressed > 0 && row_touche > -1 && col_touche > -1){
valeur_touche = bind[mode][row_touche][col_touche];
sendSPI(valeur_touche);
}
if (valeur_touche!=0) {_delay_ms(ANTI_REBOND_mS);}
On finit ensuite par reset les valeurs des variables pour recommencer le scan.
mode = 0;
row_touche = -1;
col_touche = -1;
valeur_touche = 0;
Tests du code
Lors de l'implémentation du code dans le clavier, on remarque le bon fonctionnement des leds d'indication des différents mode, cependant pour pouvoir lire les caractères tapés pour vérifier le fonctionnement, on ne peut pas lire directement via le minicom. Il faut que l'arduino les lise puis les envoie lui même. Pour cela on doit téléverser un code sur l'arduino qui sert de "relai" entre l'avr et le PC.
Interruption déclenchée à chaque réception SPI, elle conserve la data reçue dans une variable globale received et passe le flag à 1 pour signaler la réception de data.
ISR(SPI_STC_vect) {
received = SPDR; // lecture du SPI
flag = 1;
}
On calcule le baudrate ubrr (ici 9600) et on active l’émetteur UART avec UCSR0B. Pour le SPI, on mets le MISO en sortie et on active le SPI et l'interruption SPI avec SPE et SPIE.
void uart_init(void) {
uint16_t ubrr = F_CPU / 16 / 9600 - 1;
UBRR0H = (ubrr >> 8);
UBRR0L = ubrr;
UCSR0B = (1 << TXEN0);
UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}
void spi_init_slave(void) {
DDRB |= (1 << PB4);
SPCR = (1 << SPE) | (1 << SPIE); // SPI + interruption
}
On attend que le registre d'envoi soit vide puis on écrit le caractère.
void uart_send(char c) {
while (!(UCSR0A & (1 << UDRE0)));
UDR0 = c;
}
On initialise le système puis on rentre dans une boucle infinie, si un flag est détécté, on envoie la data reçue avec uart_send() puis on réinitialise le flag.
int main(void) {
uart_init();
spi_init_slave();
sei(); // activation interruptions
while (1) {
if (flag) {
uart_send(received);
flag = 0;
}
}
}
On peut désormais lire les caractères sur le minicom.
Ordonnanceur
Le code complet de l'ordonnanceur est trouvable ici sur le git. Les définitions sont trouvable dans le .h associé.
On commence par créer une structure qui représente une tâche, elle contient :
- Un pointeur de la fonction correspondante
- Sa place dans la pile pour permettre de la retrouver
- Son état soit endormi soit reveillé
- Un temps de sommeil
typedef struct{
void (*addr_fonction)(void);
uint16_t stackPointer; // Pointeur de pile
bool state; // État (actif vide ou finie)
int sleepTime;
} Process;
La tâche principale consiste à faire clignoter une led et de donner un temps de sommeil.
void led1(){
while (1){
PORTC ^= 1;
sleep(287);
}
}
//[led2,led3...]
On ajoute également une fonction idle, pour qu'il y ai toujours une tâche réveillée si les autres sont endormies, cela empêche un blocage dans une boucle infinie. Cette dernière n'est pas nécessaire dans la version la plus archaïque de l'ordonnanceur mais devient indispensable à partir du moment où on utilise notre fonction sleep().
void anti_block(){ //tache vide pour éviter de rester bloqué si toutes les taches sont endormies
while(1);
}
La fonction sleep() permet de remettre le timer de l'ISR à (F_CPU/1000*periode/diviseur - 1) et ensuite changer le temps de sommeil de la tâche actuelle par un temps donné.
void sleep(int ms_sleep){
cli();
TCNT1=max_counter-1;
liste_process[process_actuel].sleepTime = ms_sleep;
sei();
}
La fonction change_process() commence par décrémenter les temps de sommeil de toutes les tâches en liste ou mettre ce temps à 0 si il est inférieur au paramètre de décrémentation. Ensuite il parcourt toutes les tâches réveillées et avec un temps de sommeil supérieur à 0. L'utilisation de la boucle do while au lieu de while permet également de ne pas rester bloquer car le programme exécute d'abord la boucle avant de tester les conditions ce qui permet de toujours parcourir les process même si aucun d'entre eux ne respectent les conditions. Dans le cas d'un simple while, si le process ne respecte pas les conditions, on ne rentrera jamais dans la boucle, puisque on ne change jamais de process.
void change_process(){
for (int i=0 ; i<NB_PROCESS ; i++){
if (liste_process[i].sleepTime - S_DECR< 0){
liste_process[i].sleepTime = 0;
}
else{
liste_process[i].sleepTime -= S_DECR;
}
}
do{
process_actuel++;
if (process_actuel >= NB_PROCESS){
process_actuel = 0;
}
}
while (liste_process[process_actuel].sleepTime > 0 || !(liste_process[process_actuel].state));
}
La fonction ajout_tache() permet d'ajouter dynamiquement une tâche à la liste.
On désactive les interruptions puis on parcourt la liste pour trouver la première tâche endormie.
A cette place on réveille la tâche, on y place le pointeur de fonction passée en paramètre et on met à jour le stack pointer et la pile.
On réactive finalement les interruptions.
void ajout_tache(void (*fonction)(void)){
cli();
int counter = 0;
while(liste_process[counter].state){
counter++;
}
liste_process[counter].state = true;
liste_process[counter].addr_fonction = fonction;
init_stackPointer_tasks(counter);
InitialisationPile(counter);
sei();
}
D'une manière légèrement symétrique, la fonction tue_tache() retire dynamiquement une tâche de la liste.
On désactive les interruptions puis on parcourt la liste pour trouver la tâche avec la fonction correspondante à celle donnée en paramètre.
Si on la trouve, on endort la tâche et on remplace le pointeur de fonction par le pointeur NULL
On réactive finalement les interruptions.
void tue_tache(void (*fonction)(void)){
cli();
for(int counter = 0; counter<NB_PROCESS; counter++){
if(liste_process[counter].addr_fonction == fonction){
liste_process[counter].addr_fonction = NULL;
liste_process[counter].state = false;
liste_process[counter].sleepTime = 0;
}
}
sei();
}
Pour l'initialisation, on ajoute les tâches une par une avec notre fonction.
On impose les ports des leds à être en sortie.
On appelle les différentes fonctions d'initialisation notamment de stack pointer, de pile,du minuteur pour les interruptions, du SPI et d'état.
Cette dernière parcourt le nombre de tâches possible et les mets vraies si il y a un pointeur de fonction.
void initialisation(){
//setup fonctions
ajout_tache(led1);
//...leds
ajout_tache(anti_block);
//config ports
DDRC |= 0b00001001;
DDRD |= 0b10010000;
DDRB |= 0b00000001;
init_minuteur(DIVISEUR,PERIODE);
init_etat();
serial_init(SPEED);
for(int current_process = 0 ; current_process < NB_PROCESS ; current_process ++){
init_stackPointer_tasks(current_process);
InitialisationPile(current_process);
}
}
void init_etat(){
for(int i = 0; i < NB_PROCESS; i ++){
liste_process[i].state = (liste_process[i].addr_fonction != NULL);
}
}
En guise d'amélioration, on créer une fonction capable de faire choisir via le clavier, quel tâche on endort ou on réveille, ici quelle led fait clignoter ou non. Dans le cas d'un caractère autre que prévu, toutes les tâche sont endormie et, les leds clignotent brièvement pour l'indiquer. Ici on réveille les process des leds avec les touches 0 à 4 du pavé numérique, et on les désactive avec les touches 5 à 9. Toute autre touche fera clignoter les led un court laps de temps puis les éteindra et tuera les process.
void choix_led(){
char carac;
while(1){
carac = serial_get();
switch (carac){
case '0':
ajout_tache(led1);
break;
//...pour les ajouts
case '5':
tue_tache(led1);
break;
//...pour les tuer
default:
tue_tache(led1);
//...tue leds
for(int i=0; i<10;i++){/*Clignotement*/}
break;
}
}
}
