SE4Binome2025-3

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

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

Screenshot 2025-11-14 13-52-53.png

En utilisant un modèle donné nous avons fait cette schématique pour notre shield.

Routage

Nous avons routé le shield comme ceci

shield


Notre carte ressemblant à celle du binôme 1, nous utiliserons par conséquent la leur.

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.

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.

Disposition des touches

La disposition est trouvable dans le fichier bind_clavier trouvable sur le git.

Mode 1: Minuscule
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
Mode 2 : Majuscule
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
Mode 3 : Num et 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.

Carte2.png

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 :

Image du routage du clavier

Images plus en détails de la carte :

Matrice touche.png
Partie avr.png








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.

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;

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 reveillée si les autres sont endormies, cela empêche un blocage dans une boucle infinie.

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écrementer 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écrementation. Ensuite il parcourt toutes les tâches reveillées et avec un temps de sommeil supérieur à 0.

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.

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;
    }
  }
}

Vidéos test ordonnanceur






Conclusion