SE4Binome2023-3
Ordonnanceur / SE
Nous avons soudé l'ensemble des composants du shield arduino et avons testé son bon fonctionnement à l'aide de l'application Arduino. Voici ci-dessous une vidéo du clignotement des LEDs.
Vidéo du clignotement des LEDs
Nous testerons notre ordonnanceur dans un premier temps avec 3 tâches représentées par le clignotement d'une Led. Nous avons débuté par un premier programme pour vérifier le clignotement des Leds et faire fonctionner l'ISR nue. Après vérification que tout fonctionne bien, nous essayons de sauvegarder le contexte et de le reconstituer.
Après moulte tentative, nous sommes parvenus à réaliser un ordonnancuer fonctionelle qui a comme tâches le clignotement de 3 LEDs. Cependant, ce multi-tasking engendre un ralentissement des temps voulu, il faut donc géré cette partie afin de faire correspondre les délais du cahier des charges et ceux de notre réalisation.Ci-dessous, voici le code permettant de gérer l'ordonnancement :
struct task_t{
void (*addr)(void);
uint16_t sp;
int state;
};
void initTask(int taskId){
int save = SP;
SP = task[taskId].sp;
uint16_t address = (uint16_t)task[taskId].addr;
asm volatile("push %0" : : "r" (address & 0x00ff) );
asm volatile("push %0" : : "r" ((address & 0xff00)>>8) );
SAVE_REGISTER();
task[taskId].sp = SP;
SP = save;
}
void scheduler (){
currentTask ++;
if(currentTask == NB_TASK) currentTask = 0;
}
ISR(TIMER1_COMPA_vect,ISR_NAKED){
// Sauvegarde du contexte de la tâche interrompue
SAVE_REGISTER();
task[currentTask].sp = SP;
// Appel à l'ordonnanceur
scheduler();
// Récupération du contexte de la tâche ré-activée
SP = task[currentTask].sp;
RESTORE_REGISTER();
asm volatile("reti");
}
int main(){
setup();
SP = task[currentTask].sp;
task0();
}
Le main réalise d'abord le setup des LEDs, des interruptions, des tâches (leur addresse, leur pointeur de pile par exemple: 0x0600, leur état) ... puis récupère le pointeur de pile et lance manuellement la première tâche (task0). A chaque interruption de l'ISR, on sauvegarde le contexte de la tâche en cours, on choisit la prochaine tâche (task0 --> task1) et on restaure le contexte précédent associé à cette tâche. A la fin de l'interruption, la tâche voulue se lance et on attend la prochaine interruption pour continuer l'algorithme.
Cependant, vous pouvez voir ci-dessous que les tâches sont 3 fois plus longues que voulues. En effet, chaque delay est multipplié par le nombre de tâches à réaliser (ici, 3). Nous devons donc réfléchir à une méthode permettant d'endormir les tâches non sollicitées par l'ordonnanceur.
Afin d'endormir les tâches, nous écrivons une fonction wait
. Désormais, chaque structure task_t
inclut son état (endormi ou éveillé) et une structure sleep_t
qui contient deux paramètres. Ces deux paramètres sont la raison de l'endormissement de la tâche et un compteur. Le but de notre fonction est d'attendre la fin du compteur pour changer son état et réinitialiser le compteur. Cette vérification de compteur se fait dans la fonction scheduler on modifie un peu alors la fonction.
Ordonnanceur.c :
void wait(uint8_t reason, uint16_t data){
cli();
task[currentTask].state = SLEEP;
sleep_t sleep;
sleep.reason = reason;
sleep.data = data;
task[currentTask].sleep = sleep;
TCNT1 = 0;
sei();
TIMER1_COMPA_vect();
}
void scheduler (){
for(int i=0; i<NB_TASK; i++){
if(task[i].state == SLEEP && task[i].sleep.reason == DELAY_SLEEPING){
// Récupérer la différence de temps
uint16_t difftime_ms = 20;
if(TCNT1 != 0){
difftime_ms = TCNT1*200/OCR1A/10; // On multiplie par 10 pour ne pas avoir de problème avec les nombres flottants
TCNT1 = 0;
}
task[i].sleep.data -= difftime_ms;
if(task[i].sleep.data <= 0){
task[i].state = AWAKE;
}
}
}
do{
currentTask ++;
if(currentTask >= NB_TASK) currentTask = 0; // Attention si tous les processus sont à l'arrêt
}while(task[currentTask].state == SLEEP);
}
Ordonnanceur.h :
#define SLEEP 0
#define AWAKE 1
#define DELAY_SLEEPING 0
typedef struct sleep_t{
uint8_t reason;
uint16_t data;
}sleep_t;
typedef struct task_t{
void (*addr)(void);
uint16_t sp;
uint8_t state;
sleep_t sleep;
}task_t;
Voici donc ci-dessous le résultat en contrôlant l'état des LEDs (ce dernier ressemble ostensiblement à la vidéo précédente, mais cette fois ci, les temps voulus sont exacts)
Carte FPGA / VHDL
Ayant voulu nous concentrer sur notre carte électronique clavier afin d'obtenir un périphérique (quasiment) parfaitement opérationnel, nous ne nous sommes pas attardés sur la carte FPGA en VHDL et ne l'avons pas entamé.
Carte électronique numérique
L'ensemble de nos fichiers sont disponible dans notre git.
Type de carte choisi
Nous avons choisi la carte fille gérant le clavier. Nous allons donc souder et programmer une carte sur laquelle sera branché un clavier en USB.
Schematic et routage de la carte
Nous avons réalisé le schematic de notre carte en nous basant sur ce projet. Nous avons pris la décision d'utiliser le même port USB femelle pour brancher le clavier et programmer la carte avec l'ordinateur (plutôt qu'un femelle pour le clavier et un mâle pour la programmation).
Après avoir assigné les correctes footprint à chacun des éléments, nous avons commencé le routage de notre carte fille. Nous avons fait le choix de connecter chaque pin seul à un test point, ce qui nous a valu en premier lieu de sacré problème quant à l'organisation de l'espace que prenaient ces test point sur le PCB ! Après vérifications auprès de Mrs Boé et Redon, nous avons supprimé certains test points prenant trop de place et avons déplacer certains composants (rapprochage du crystal et des condensateur du microprocesseur, perfectionnement de la carte, ajout de vias pour répartir le transfert de courant entre les deux faces pour la masse...) nous avons généré les deux fichiers nécessaires à l'impression par un prestataire extérieur et avons, en attendant sa réception, commencé à travailler notre ordonnanceur.
Soudage de la carte
Nous avons reçu notre carte en 5 exemplaires le 24/10, et avons décidé de continuer l'ordonnanceur avant de commencer la soudure.
En ce grand jour du 15/11, nous avons enfin reçu notre AT90USB647 et pourrons commencer la soudure à la prochaine séance ! Au cours du premier test, notre carte n'était pas reconnu et nous avions des valeurs de tensions incohérentes en différents points. Après different soudage-resoudage de nombreux composants, nous avons compris que ceci venait en fait des boutons. En effet, nous avons choisi un composant incorrect sur notre schematic ce qui a entrainé des erreurs sur notre routage, ce qui engendre le fait que le port reset et le port HWB était constamment en court-circuit avec le ground (ceci explique cela...). Pour résoudre le problème, nous avons resoudé nos boutons avec une rotation de 90° et avons mis à jour notre schematic et le routage qui va avec. Voici donc la deuxième version des deux boutons (ici on prend l'exemple du reset) :
Nous avons commencé les premiers tests le 21/11et ces derniers furent concluant. Notre carte est reconnu par le terminal avec la commande lsusb
et on a pu faire clignoter nos LEDs de test.
Programmation et familiarisation SPI en amont
Nous nous sommes familiariser avec le SPI avant de se lancer sur notre carte. Nous essayons donc d'utiliser l'afficheur 7 segments pour faire un compteur.
Tout d'abord il faut initialiser le SPI. En modifiant certains registres nous permettons à notre carte mère d'utiliser le SPI. Attention à bien mettre la borche SS de base (même si dans notre application elle n'est pas utilisée) en sortie sinon le SPI ne fonctionne pas.
Ensuite en lisant la datasheet de l'afficheur 7 segments on comprend qu'il est assez simple à utiliser et son fonctionnement est basé uniquement sur des instructions séquentielles en SPI. Attention vitesse de travail max 250kHz soit la clock de l'atmega diviser par au minimum 64. On se sert de ce tableau qui nous sera aussi très utile plus tard.
L'exemple de suite d'instruction suivant permet d'afficher un compteur.
void Write7Segment(){
counter = 0;
initSPI();
while(1){
//Calcul
counter++;
if (counter > 9999) counter = 0;
uint8_t digit1 = counter/1000;
uint8_t digit2 = (counter - digit1*1000) / 100;
uint8_t digit3 = (counter - digit1*1000 - digit2*100) / 10;
uint8_t digit4 = (counter - digit1*1000 - digit2*100 - digit3*10);
selectSlaveSPI(&PORTC, SS3);
transferSPI(0x76);
transferSPI(digit1);
transferSPI(digit2);
transferSPI(digit3);
transferSPI(digit4);
wait(DELAY_SLEEPING, 100);
unselectSlaveSPI(&PORTC, SS3);
}
}
Test à 500kHz:
Test à 250kHz:
Test à 125kHz:
A 500kHz ça ne fonctionne pas logique ! Par contre on peut voir que curieusement le comportement est différent à une vitesse plus faible. Il parait logique qu'à une vitesse trop élevée il peut apparaitre certains bugs mais à plus faible vitesse cela reste un mystère pour nous... La vitesse de transmission viendra d'ailleurs nous poser beaucoup d'interrogation dans la suite de ce projet nottament certains bugs pour récupérer les touches de notre clavier.
Programmation de la carte
A l'aide de la LUFA et de leur démo KeyboardHost
, nous allons tenter de reconnaître les touches tapés sur un clavier. Pour vérifier cela et débuggé notre code, nous avons ajouté 8 LEDs de tests destinés à afficher la valeur binaire des caractère ASCII correspondant.
Pour ce faire dans le fichier de démo lufa, nous avons implémenté la conversion des keycodes en caractères ascii. Il a été plus simple pour nous de convertion en utilisant le layout anglais qwerty. Il a donc fallu effectué une conversion également pour certaines touches afin de passer de qwerty à azerty. Pour finir nous considérons que de base les clés sont celles du bas lowerKey abcd. Si un événement vient mettre le drapeau upper (mode majuscule) à 1 alors nous passons sur les upperkey ABCD.
On se base sur ce layout:
On effectue la conversion pour ce layout:
A l'aide de son keycode nous avons codé la mise en place du mode majuscule grâce au capslock. Nous pouvons maintenant recevoir sur notre carte les caractères classiques (a à z, 0 à 9, espace, entrée, tabulation...)
Après quelques complications sur la recherche des keycodes (vive la lufa !) nous pouvons également désormais activer le mode majuscule avec les touches shift droite et gauche. Nous avons également rajouté la possibilité de taper les caractères spéciaux associés aux nombres et à droite du clavier. Malheureusement, les caractères ¨, µ, £ et § n'étant pas présent dans la table ASCII, ils ne seront pas disponible sur notre clavier.
Grâce aux leds et à convertisseur ascii nous vérifions que la reconnaissance des touches est bonne.
les fonctions pour les leds et pour le reste de notre projet se trouvent dans le fichier inout.c afin de ne pas surcharger le fichier keyboardHost.
Nous souhaitons ensuite communiquer avec la carte mère par le bus SPI. Nous vérifions donc que nous réussissons à lancer une interruption spi sur la carte fille. Nous vérifions également que nore carte fille peut envoyer une interruption par la ligne d'interruption du connecteur HE10. Cette ligne servira plus tard à indiquer qu'il y a des touches dans le buffer et qu'il faut que la carte mère interroge notre carte dès que possible.
Ensuite à l'aide d'un switch nous décodons l'instructions reçu lors d'une interruption SPI.
ISR(SPI_STC_vect) {
cli();
uint8_t receivedData = SPDR;
switch (receivedData){
case 0x00:
SPDR = 0x01;
break;
case 0x01:
if(!sizeSendFlag){
SPDR = sizeBuffer();
sizeSendFlag = 1;
break;
}
SPDR = dequeue();
if(!sizeBuffer()){
setLowOutput(&PORTB, INT);
sizeSendFlag = 0;
}
break;
default:
SPDR = dequeue();
if(!sizeBuffer()){
setLowOutput(&PORTB, INT);
}
break;
}
sei();
}
3 instructions possibles :
- 0x00, nous envoyons 0x01 correspondant à notre type (clavier).
- 0x01, nous envoyons d'abord le nombre de touche dans le buffer puis nous envoyons à chaque nouvelle requète la dernière touche du buffer.
- toutes autres instructions, nous envoyons la dernière clé du buffer.
Lorsqu'on dépile on vérifie si le buffer n'est pas vide. S'il l'est on remet à bas notre ligne d'interruption.
Nous ajoutons en plus un buffer géré en FIFO. Celui-ci retient les clés tapées la tête de buffer et la queue. Retenir la tête et la queue et la faire bouger nous permet de limiter les opérations. Autrement il aurait fallu décaler toutes les clés dans le buffer en cas de dequeue ou enqueue.
uint8_t isEmpty(){
return buffer.head == -1;
}
uint8_t isFull() {
return (buffer.tail + 1) % MAX_DATA == buffer.head;
}
uint8_t sizeBuffer(){
if(isEmpty()) return 0;
if(buffer.tail < buffer.head)
return MAX_DATA - (buffer.head - buffer.tail) + 1;
return buffer.tail - buffer.head + 1;
}
void enqueue(char key){
if (isFull()) {
return;
}
buffer.tail = (buffer.tail + 1) % MAX_DATA;
buffer.data[buffer.tail] = key;
if (isEmpty()) {
buffer.head = 0;
}
}
char dequeue(){
if (isEmpty()) {
return 0x00;
}
char key = buffer.data[buffer.head];
if (buffer.head == buffer.tail) {
// Le buffer est maintenant vide
buffer.head = -1;
buffer.tail = -1;
} else {
buffer.head = (buffer.head + 1) % MAX_DATA;
}
return key;
}
Enfin nous ajoutons une fonction qui permet de connaître la taille de la FIFO.
uint8_t sizeBuffer(){
if(isEmpty()) return 0;
if(buffer.tail < buffer.head)
return MAX_DATA - (buffer.head - buffer.tail) + 1;
return buffer.tail - buffer.head + 1;
}
Nos leds de test ici s'avèrent très pratique et nous permettent entre autre de vérifier notre fonction de taille de buffer.
Primitive carte mère
Afin de simplifier l'utilisation de notre carte fille nous avons codé différentes primitives pour la carte mère elles se retrouvent toutes dans la librairie libdevice que nous avons testé sur notre carte mère à nous. Pour le groupe s'occuppant de la carte mère il suffira donc d'inclure cette librairie et d'utiliser les fonctions à sa guise (attention par contre à adapter les broches dans le .h).
note : la librairie est compilé par son propre makefile.
device.c/.h
Ce fichier n'est pas spécifique à notre carte et peut être utilisé par toutes les autres cartes filles.
il comporte tout d'abord les fonctions de gestion de spi:
- initSPI: initialise la transmission SPI. Attention à adapter les ports d'entrées et de sortie à la carte mère utilisée.
- selectSlaveSPI / unselectSlaveSPI: sélectionne/désélectionne le périphérique à qui transmettre l'info.
- transferSPI: envoie et récupère l'information sur le bus SPI.
Par la suite nous ajoutons une seconde couche plus spécifique à notre application. Elle repose sur le typage de chaque composant exemple pour notre clavier le type est 0x01:
- initConnectorsList: crée une liste nous permettant de sauvegarder des informations sur les connecteurs (leurs brochese et leur périphérique attaché). De plus cette fonction vient sonder chaque He10 afin de connaître le périphérique attaché permettant d'être plus modulaire leur de la connexion.
- indexDevice: renvoie l'index du périphérique recherché.
- transferDataTo: se sert de indexDevice et des fonctions spi pour envoyer une données à un périphérique en particulier.
- checkInterrupt: vient sonder un périphérique pour savoir si sa ligne d'interruption est à l'état haut.
Pour nous simplifier la tâche on code des fonctions utilent comme getDeviceList qui renvoie une liste du type de chaque périphérique et initDevice qui initialise le SPI et la liste des périphériques.
keyboard.c/.h
Ce fichier quant à lui est spécifique à notre périphérique et se base sur device.c.
Il permet à la carte mère de récupérer une clé à l'aide de grabKey. La récupération de plusieurs clés et aussi possible grâce à grabKeys pour se faire nous récupérons d'abord la taille du tableau comprenant les clés pour allouer de l'espace et savoir le nombre de requète spi à effectuer. Cela est fait grâce à bufferSize.
Résultats et axes d'amélioration
Le projet pico-ordinateur nous a permis d'expérimenter une fois de plus la gestion d'un projet liant électronique et informatique embarquée. Dans notre cas, nous nous sommes concentrer sur le clavier de l'ordinateur, que nous pourrons considérer comme quasiment fonctionnel quoiqu'un peut perfectible. En effet, nous avons remarqué certains décalage de touches sur le gestionnaire minicom (Ce décalage a été réglé en dernière mintue voir vidéo + explication). Ces décalages sont présent, d'après nous, suite à un problème de gestion de la connexion SPI. Nous n'avons pas pu vérifier la compatibilité et la bonne gestion de notre clavier directement sur la carte mère du binôme concerné. Nous aurions pu, pour s'assurer du bon fonctionnement de la liaison entre cartes clavier, mère et écran, afficher les caractères tapés au clavier. Cependant, notre clavier a lui tout seul permet tout de même d'écrire à une fréquence correcte les caractères souhaités.
Voici ci-dessous une vidéo montrant le clavier en fonctionnement avec l'outil minicom notre carte mère récupère les caractères ascii des clefs tapées et les renvoie sur le port série. Le but final aurait été de les imprimer sur la carte écran et même de pouvoir les retenir pour taper des commandes.
code carte mère utilisant les primitives codées en amont:
initSerial();
initDevice();
while(1){
if(checkInterrupt(KEYBOARD)){
serialWrite(grabKey());
}
}
vidéo de test avec ce code:
On peut voir qu'on obtient sur minicom le résulat suivant:Device : ///1/
hello world ! helowrl !
La fonction permettant de vérifier où se trouve le clavier fonctionne bien. En effet il était sur le quatrième connecteur HE10 d'où ///1/.
A vitesse de frappe faible la récupération de touche s'effectue bien par contre dès qu'on augmente la cadence le résultat est faussé. Nous pensons que cela est due à des zones de code critique. Si au moment de rentrer une touche dans le buffer une interruption spi se produit alors il se peut que la touche soit perdue.
De plus comme nous l'avons expliqué plus haut curieusement à vitesse de SPI plus faible certaines touches sont remplacées par une touche avec un code ascii d'un bit d'écart (erreur transmission SPI). Nous avons donc gardé une cadence élevée.
Références utiles
Lien du git : git.
Lien Lufa pour retrouver l'ensemble des keycodes : Vive la lufa