« SE4Binome2025-6 » : différence entre les versions

De projets-se.plil.fr
Aller à la navigation Aller à la recherche
 
(9 versions intermédiaires par 2 utilisateurs non affichées)
Ligne 9 : Ligne 9 :
== Carte Shield ==
== Carte Shield ==
La première étape est de réaliser un shield au cas où notre carte mère s'avérerait non fonctionnelle, afin de ne pas bloquer l'avancée des groupes des cartes filles.
La première étape est de réaliser un shield au cas où notre carte mère s'avérerait non fonctionnelle, afin de ne pas bloquer l'avancée des groupes des cartes filles.
<p style="clear: both;" />
=== Hardware ===
=== Hardware ===


Ligne 53 : Ligne 51 :
# si la carte SD est défaillante en testant le programme avec une autre carte SD. On teste aussi sur un lecteur de carte SD pour voir si elle est détectée sur le pc ce qui est le cas -> le pb ne vient pas de là non plus.
# si la carte SD est défaillante en testant le programme avec une autre carte SD. On teste aussi sur un lecteur de carte SD pour voir si elle est détectée sur le pc ce qui est le cas -> le pb ne vient pas de là non plus.
# on s'intéresse maintenant à l'aspect matériel, on vérifie les soudures -> toujours pas de souci particulier.
# on s'intéresse maintenant à l'aspect matériel, on vérifie les soudures -> toujours pas de souci particulier.
# schématique et routage de la carte : on s'aperçoit alors que l'on a inversé le sens du 74LVC125 de l'unité U1A pour la conversion de niveau logique du MOSI en appuyant par erreur sur le raccourci clavier x qui inverse en "miroir" le sens du composant. Remarque : on aurait aussi vraiment dû mettre le convertisseur de niveau logique sur la carte mémoire pour voir le problème plus vite. Conséquence : le routage n'est pas correct, on rajoute donc des fils pour faire les bonnes connexions et on coupe au cutter les mauvaises.
# schématique et routage de la carte : on s'aperçoit alors que l'on a inversé le sens du 74LVC125 de l'unité U1A pour la conversion de niveau logique du MOSI en appuyant par erreur sur le raccourci clavier x qui inverse en "miroir" le sens du composant. Le routage à été modifié sur kicad par la suite et l'erreur réparé comme on peut le voir sur la photo.


=== Software ===
=== Software ===
Ligne 62 : Ligne 60 :
===== Ordonnanceur =====
===== Ordonnanceur =====
Maintenant que notre shield est fonctionnel, nous pouvons réaliser notre ordonnanceur. A voir ici : https://gitea.plil.fr/ahouduss/SE4-Pico-B6/src/branch/master/03-Code/Carte_shield/ordonnanceur
Maintenant que notre shield est fonctionnel, nous pouvons réaliser notre ordonnanceur. A voir ici : https://gitea.plil.fr/ahouduss/SE4-Pico-B6/src/branch/master/03-Code/Carte_shield/ordonnanceur
Faire un ordonnanceur sur une architecture AVR est nécessaire si l'on souhaite ensuite le faire sur une architecture ARM Cortex M4 qui est bien plus complexe.


Notre ordonnanceur est structuré de la manière suivante :
Notre ordonnanceur est structuré de la manière suivante :
Ligne 75 : Ligne 75 :
Description des fichiers et fonctions implémentées :
Description des fichiers et fonctions implémentées :


====== HARWARE ======
====== HARDWARE ======
La première étape est de faire clignoter les leds qui serviront de futures tâches. On implémente donc les fonctions suivantes : <syntaxhighlight lang="c">
<syntaxhighlight lang="c">
#define LEDs_PORT &PORTD
#include "hardware.h"
#include <avr/io.h>
#include <util/delay.h>


#define LED_CS1 3
#include "../USART/usart.h"
#define LED_CS2 2
#define LED_CS3 1
#define LED_CS4 0


void toggleLedCS1(){
  *LEDs_PORT ^= (1 << LED_CS1);
}


void toggleLedCS2(){
void setupClock() {
  *LEDs_PORT ^= (1 << LED_CS2);
    // Activer possibilité de changer le prescaler
}
    CLKPR = (1 << CLKPCE);


void toggleLedCS3(){
    // Choix diviseur
  *LEDs_PORT ^= (1 << LED_CS3);
    CLKPR = 0;
}
}


void toggleLedCS4(){
void setupPin(volatile uint8_t *PORTx, volatile uint8_t *DDRx, uint8_t pin, pinmode mode) {
  *LEDs_PORT ^= (1 << LED_CS4);
   switch (mode) {
}
 
</syntaxhighlight>On réutilise ensuite la fonction setupPin qui permet d'initialiser nos leds.<syntaxhighlight lang="c">
typedef enum {
  INPUT,
  INPUT_PULL_UP,
  OUTPUT,
} pinmode;
 
#define LEDs_DDR &DDRD
 
void setupPin(volatile uint8_t *PORTx, volatile uint8_t *DDRx, uint8_t pin, pinmode mode) {
   switch (mode) {
   case INPUT: // Forcage pin à 0
   case INPUT: // Forcage pin à 0
     *DDRx &= ~(1 << pin);
     *DDRx &= ~(1 << pin);
Ligne 123 : Ligne 106 :
   }
   }
}
}
</syntaxhighlight>On initialise ensuite l'horloge :<syntaxhighlight lang="c">
 
void setupClock() {
int readPin(volatile uint8_t *PINx, uint8_t pin) {
    // Activer possibilité de changer le prescaler
  return (*PINx & (1 << pin));
    CLKPR = (1 << CLKPCE);
    // Choix diviseur
    CLKPR = 0; 
}
}
</syntaxhighlight>Puis l'hardware complet :<syntaxhighlight lang="c">
 
void setupHardware(){
void setupHardware(){
   setupClock();
   setupClock();
Ligne 141 : Ligne 121 :
   init_usart();
   init_usart();
}
}
</syntaxhighlight>


====== USART ======
void toggleLedCS1(){
Dans le dossier USART, on définit les fonctions basiques pour initialiser, envoyer et recevoir depuis l'usart. L'usart nous servira par la suite pour certaines tâches.<syntaxhighlight lang="c">
  *LEDs_PORT ^= (1 << LED_CS1);
#define BAUD_RATE 9600
}
#define F_CPU 16000000UL
#define UBRR_VALUE ((F_CPU/16/BAUD_RATE)-1)


void init_usart() {
void toggleLedCS2(){
   // Serial Initialization
   *LEDs_PORT ^= (1 << LED_CS2);
  /*Set baud rate 9600 */
  UBRR0H = (unsigned char)(UBRR_VALUE >> 8);
  UBRR0L = (unsigned char)UBRR_VALUE;
 
  /* Enable receiver and transmitter */
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);
 
  /* Frame format: 8data, No parity, 1stop bit */
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}
}


void usart_send(unsigned char data) {
void toggleLedCS3(){
   while (!(UCSR0A & (1 << UDRE0)));
   *LEDs_PORT ^= (1 << LED_CS3);
  UDR0 = data;
}
}


unsigned char usart_receive() {
void toggleLedCS4(){
   while (!(UCSR0A & (1 << RXC0)));
   *LEDs_PORT ^= (1 << LED_CS4);
  return UDR0;
}
}
</syntaxhighlight>


====== TASK ======
On pense ensuite aux tâches que l'on va implémenter dans notre ordonnanceur.


On implémente des tâches pour les leds :<syntaxhighlight lang="c">
</syntaxhighlight>Et son .h :<syntaxhighlight lang="c">
#ifndef HARDWARE_H
#define HARDWARE_H


void taskCS1() {
#include <stdint.h>
    while(1){    
 
        toggleLedCS1();
// ------------------ Enum ------------------ //
        _delay_ms(1000);
typedef enum {
    }
   INPUT,
}
  INPUT_PULL_UP,
  OUTPUT,
} pinmode;
 
// ------------------ LEDs ------------------ //
#define LEDs_PORT &PORTD
#define LEDs_DDR &DDRD
#define LEDs_PIN PIND
 
#define LED_CS1 3
#define LED_CS2 2
#define LED_CS3 1
#define LED_CS4 0


void taskCS2() {   
    while(1){ 
        toggleLedCS2();
        _delay_ms(500);
    } 
}


void taskCS3() {
// ------------------ Prototypes ------------------ //
    while(1){ 
void setupClock();
        toggleLedCS3();
        _delay_ms(500);
    } 
}


void taskCS4() {
void setupPin(volatile uint8_t* PORTx, volatile uint8_t* DDRx, uint8_t pin, pinmode mode);
    while(1){ 
int readPin(volatile uint8_t* PINx, uint8_t pin);
        toggleLedCS4();
void setupHardware();
        _delay_ms(250);
    } 
</syntaxhighlight>et pour le série :<syntaxhighlight lang="c">


void taskSendSerialA() {
void toggleLedCS1();
  while (1) {
void toggleLedCS2();
    usart_send('A');
void toggleLedCS3();
    usart_send('\n');
void toggleLedCS4();
    usart_send('\r');
    _delay_ms(500);
  }
}


void taskSendSerialB() {
void taskToggleCS1();
  while (1) {
void taskToggleCS2();
    usart_send('B');
void taskToggleCS3();
    usart_send('\n');
void taskToggleCS4();
    usart_send('\r');
    _delay_ms(250);
  }
}
</syntaxhighlight>On définit ensuite la structure générale pour les tâches.


Une tâche possède :
#endif
</syntaxhighlight>Dans la librairie hardware.c nous avons un ensemble de fonctions liées à notre matériel pour son initialisation et son contrôle ou sa lecture (de pin).


* un pointeur de fonction avec le nom de la tâche
====== USART ======
* un pointeur pour l'adresse mémoire afin de savoir où est située la tâche dans la pile
Dans la librarie USART, on définit les fonctions basiques pour initialiser, envoyer et recevoir depuis l'usart. L'usart nous servira par la suite pour certaines tâches.<syntaxhighlight lang="c">
* l'état de la tâche : actif ou non (endormi)
#include "usart.h"
* le temps pour lequel la tâche reste endormie
#include <avr/io.h>
<syntaxhighlight lang="c">
#include <stdint.h>
typedef enum{
#include <util/delay.h>
    AWAKE,
    SLEEP,
} state_task;


typedef struct {
void init_usart() {
    void (*taskAddress)(void);  // fonction de la tache
  // Serial Initialization
    uint16_t stackPointer;      // pointeur de pile
  /*Set baud rate 9600 */
    state_task state;              // AWAKE ou SLEEP
  UBRR0H = (unsigned char)(UBRR_VALUE >> 8);
    uint16_t sleepTime;         // temps restant en ms
  UBRR0L = (unsigned char)UBRR_VALUE;
} process;


  /* Enable receiver and transmitter */
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);


</syntaxhighlight>On définit donc un tableau de tâches et une constante pour savoir le nombre de tâches que l'on a. <syntaxhighlight lang="c">
  /* Frame format: 8data, No parity, 1stop bit */
process task[] = {
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
    // {taskSendSerialA, 0x0780, AWAKE, 0},
}
    {taskCS1, 0x0730, AWAKE, 0},
    // {taskSendSerialB, 0x06E0 , AWAKE, 0},
    // {taskCS2, 0x0690, AWAKE, 0},
    // {taskCS3, 0x06E0, AWAKE, 0},
    // {taskCS4, 0x0620, AWAKE, 0},
};


const uint8_t nbTasks = sizeof(task)/sizeof(task[0]);
void usart_send(unsigned char data) {
</syntaxhighlight>
  while (!(UCSR0A & (1 << UDRE0)));
  UDR0 = data;
}


====== ORDONNANCEUR ======
unsigned char usart_receive() {
Pour débuter notre ordonnanceur, on commence par créér un minuteur afin de faire clignoter deux leds à des périodes différentes. <syntaxhighlight lang="c">
  while (!(UCSR0A & (1 << RXC0)));
void init_timer1(int diviseur, long periode_ms){
  return UDR0;
    tick_ms = periode_ms;
}


    TCCR1A = 0;
</syntaxhighlight>Et usart.h :<syntaxhighlight lang="c">
    TCCR1B = (1 << WGM12); // CTC mode
#ifndef USART_H
#define USART_H


    switch(diviseur){
// Baud rate variable que l'on peut modifier
        case 64:  TCCR1B |= (1<<CS11)|(1<<CS10); break;
#define BAUD_RATE 9600
        case 256: TCCR1B |= (1<<CS12); break;
    }


    OCR1A = (F_CPU / diviseur) * periode_ms / 1000;
#define F_CPU 16000000UL
    TCNT1 = 0;
#define UBRR_VALUE ((F_CPU / 16 / BAUD_RATE) - 1)
    TIMSK1 = (1 << OCIE1A);
}
</syntaxhighlight>Une fois que le minuteur a atteint son nombre de "ticks", il appelle l'ISR : Interrupt Service Routine, qui prend le relai.


L'ISR a besoin de la pile, on implémente donc une fonction afin d'initialiser la pile. <syntaxhighlight lang="c">
void init_usart();
void init_pile(int n){
void usart_send(unsigned char data);
    uint16_t savedSP = SP;
unsigned char usart_receive();
    uint16_t addr = (uint16_t)task[n].taskAddress;


    SP = task[n].stackPointer;
#endif


    // PC (low puis high)
</syntaxhighlight>
    asm volatile("push %A0" :: "r"(addr));
    asm volatile("push %B0" :: "r"(addr));


    // r0-r31
====== TASK ======
    for(int i=0; i<32; i++)
Task.c :<syntaxhighlight lang="c">
        asm volatile("push __zero_reg__");
#include "../HARDWARE/hardware.h"
#include "../ORDONNANCEUR/ordonnanceur.h"
#include "../USART/usart.h"


    // SREG = I=1
#include "task.h"
    uint8_t s = 0x80;
    asm volatile("push %0" :: "r"(s));


    task[n].stackPointer = SP;
process task[] = {
     SP = savedSP;
    // {taskSendSerialA, 0x0780, AWAKE, 0},
}
    {taskCS1, 0x0730, AWAKE, 0},
</syntaxhighlight>
    {taskSendSerialB, 0x06E0, AWAKE, 0},
     // {taskCS2, 0x0690, AWAKE, 0},
    // {taskCS3, 0x06E0, AWAKE, 0},
    // {taskCS4, 0x0620, AWAKE, 0},
};


L'ISR procède ainsi :
const uint8_t nbTasks = sizeof(task) / sizeof(task[0]);


* Elle commence par sauvegarder les registres grâce à la fonction assembleur SAVE_REGISTERS définie dans ordonnanceur.h, qui permet de sauvegarder les registres de la tâche interrompue
void init_tasks() {
* On appelle ensuite l'ordonnanceur qui définit la manière dont les tâches sont exécutées.
  for (int i = 0; i < nbTasks; i++) {
* On restore ensuite le contexte, et tous les registres de la tâche que l'on va exécuter.
     init_pile(i);
<syntaxhighlight lang="c">
  }
ISR(TIMER1_COMPA_vect, ISR_NAKED){
}
      
    /* Sauvegarde du contexte de la tâche interrompue */
    SAVE_REGISTERS();
    task[currentTask].stackPointer = SP;


     /* Appel à l'ordonnanceur qui choisi la prochaine tache */
void taskCS1() {
     scheduler();
  while (1) {
     toggleLedCS1();
     _delay_ms(1000);
  }
}


    /* Récupération du contexte de la tâche ré-activée */
void taskCS2() {
     SP = task[currentTask].stackPointer;
  while (1) {
     RESTORE_REGISTERS();
     toggleLedCS2();
     _delay_ms(500);
  }
}


     asm volatile("reti");
void taskCS3() {
  while (1) {
    toggleLedCS3();
     _delay_ms(500);
  }
}
}
</syntaxhighlight>L'ordonnanceur est ici round-robin, il exécute donc les tâches les unes après les autres sans priorité pour certaines tâches.<syntaxhighlight lang="c">
 
void scheduler(void){
void taskCS4() {
     currentTask = (currentTask + 1) % nbTasks;
  while (1) {
     toggleLedCS4();
    _delay_ms(250);
  }
}
}


</syntaxhighlight>Fonction afin de mettre une tâche en sommeil :<syntaxhighlight lang="c">
void taskSendSerialA() {
void sleep_ms(uint16_t t) {
  while (1) {
     task[currentTask].state = SLEEP;
     usart_send('A');
     task[currentTask].sleepTime = t / tick_ms;
    usart_send('\n');
}
    usart_send('\r');
</syntaxhighlight>
     _delay_ms(500);
  }
}


====== MAIN ======
void taskSendSerialB() {
Dans le fichier main.c, on appelle diverses fonctions notamment le timer, qui appellera donc l'ISR puis le scheduler. On commence également par charger la première tâche sur la pile.<syntaxhighlight lang="c">
   while (1) {
int main(void) {
    usart_send('B');
   cli(); // désactiver interruptions
    usart_send('\n');
  setupHardware();
    usart_send('\r');
  // initialisation des piles
    _delay_ms(250);
  init_tasks();
   }
  // TIMER1 config à 20 ms
  init_timer1(64, 20);
  // charger la pile de la premi??re t??che
  SP = task[0].stackPointer;
  RESTORE_REGISTERS();
  asm volatile("reti");
   return 0;
}
}
</syntaxhighlight>


==Carte mère==
</syntaxhighlight>Task.h: <syntaxhighlight lang="c">
La deuxième carte à réaliser est la carte mère avec une spécificité cependant, à savoir une puce STM32F410R8T6 en tant que microprocesseur.
#ifndef TASK_H
#define TASK_H
 
typedef enum {
  AWAKE,
  SLEEP,
} state_task;


Remarque : M. Redon a également fait une carte mère mais basée sur un ATSAMD21G8A-A. Celle-ci pourra nous être utile si on rencontre des soucis pour le code avec notre stm32.
typedef struct {
=== Hardware ===
  void (*taskAddress)(void); // fonction de la tache
  uint16_t stackPointer;    // pointeur de pile
  state_task state;          // AWAKE ou SLEEP
  uint16_t sleepTime;        // temps restant en ms
} process;


==== Microprocesseur ====
extern process task[];
On utilise la puce STM32F410R8T6.
extern const uint8_t nbTasks;


===== Signification du nom de la puce =====
void init_tasks();
Signification (cf p 134 de la short datasheet) :
void task_led(void (*toggleFunc)(void), uint16_t ms);


Arm based 32-bit microcontroller
void taskCS1();
void taskCS2();
void taskCS3();
void taskCS4();
void taskSendSerialA();
void taskSendSerialB();


* F = General-purpose
#endif
* R = 64 pins
</syntaxhighlight>Une tâche possède :
* 8 = 64 Kbytes of Flash memory
* T = package LQFP
* 6 = Industrial temperature range, - 40 to 85 °C


===== Datasheets =====
* un pointeur de fonction avec le nom de la tâche
Datasheet de la puce  STM32F410R8T6 :
* un pointeur pour l'adresse mémoire afin de savoir où est située la tâche dans la pile
[[Fichier:STM32 datasheet.pdf|left|400px|alt=STM32_datasheet|vignette|STM32_datasheet]]
* l'état de la tâche : actif ou non (endormi)
<p style="clear: both;" />On peut également retrouver des pages supplémentaires afin de bien dimensionner notre quartz et les capacités aux alentours.
* le temps pour lequel la tâche reste endormie
On définit un tableau de tâches ainsi : <syntaxhighlight lang="c">
process task[] = {
    // {taskSendSerialA, 0x0780, AWAKE, 0},
    {taskCS1, 0x0730, AWAKE, 0},
    // {taskSendSerialB, 0x06E0 , AWAKE, 0},
    // {taskCS2, 0x0690, AWAKE, 0},
    // {taskCS3, 0x06E0, AWAKE, 0},
    // {taskCS4, 0x0620, AWAKE, 0},
};


Document AN2867- Guidelines for oscillator design on STM8AF/AL/S and STM32 MCUs/MPUs à retrouver sur le lien suivant : https://www.st.com/en/microcontrollers-microprocessors/stm32f410/documentation.html
const uint8_t nbTasks = sizeof(task)/sizeof(task[0]);
<p style="clear: both;" />
</syntaxhighlight>Le nombre de tâche est calculé automatique pour ne pas s'embêter à le faire à chaque fois que l'on modifie de le tableau de tâche.


==== Schématique ====
Si certaines tâches sont commentés c'est parceque sur cette 1ere version d'ordonnanceur, nous avons fais au plus simple sans contrôle de tache endormi, nous ne pourrons donc pas avoir un résultat propre avec plusieurs tâches simultanément.
Notre schématique est composée de quatres sous feuilles, respectivement pour l'alimentation, le microcontrôleur, la carte mémoire et les cartes filles.


===== Alimentation =====
====== ORDONNANCEUR ======
L'alimentation se fait via du 5V et est ensuite directement convertie en 3,3V par le biais du régulateur afin d'alimenter le microcontrôleur. C'est pourquoi les bus de données USB ne sont pas utilisées car l'USB servira ici uniquement à l'alimentation et pas à la transmission de données. On ajouté également des mounting holes pour fixer la carte.
ordonnanceur.c : <syntaxhighlight lang="c">
#include "ordonnanceur.h"
#include <avr/interrupt.h>


===== Carte mémoire =====
#include "../HARDWARE/hardware.h"
La carte mémoire ou carte fille SD est sensiblement la même que celle pour le shield. On a juste rajouté une capacité de découplage car la carte SD va recevoir et envoyer beaucoup de données rapidement.


===== Microcontrôleur =====
int currentTask = 0;
Le microcontrôleur est composé de beaucoup de broches dédiées à l'alimentation, aux horloges, aux boots, à la communication, aux cartes filles, aux switchs, aux leds et au JTAG (voir sections suivantes).
uint16_t tick_ms = 0;


====== Alimentation ======
// ------------------ TIMER ------------------ //
Les broches VDD servent à l'alimentation numérique et VDDA à l'alimentation analogique, ici séparée pour filtrer de manière plus précise car  plus sensible que le numérique. En effet, pour filtrer les hautes fréquences en numérique, les capacités de découplage suffisent alors qu'en analogique le signal d'entrée nécessite une gestion plus précise avec une ferrite.
void init_timer1(int diviseur, long periode_ms) {
  tick_ms = periode_ms;


====== Horloges ======
  TCCR1A = 0;
On a ici deux horloges :
  TCCR1B = (1 << WGM12); // CTC mode


* Première horloge : on peut soit choisir l'oscillateur RC de 16 MHz ou une horloge externe comprise entre 4 et 26 MHz (p18/142)  
  switch (diviseur) {
* Deuxième horloge : horloge pour le temps réel de 32kHZ (donc pas une application qu'on vise) (p22/142)
  case 64:
    TCCR1B |= (1 << CS11) | (1 << CS10);
    break;
  case 256:
    TCCR1B |= (1 << CS12);
    break;
  }


====== Boot et configuration ======
  OCR1A = (F_CPU / diviseur) * periode_ms / 1000;
Les boot 0 et 1 permettent de choisir la partie ou le bloc mémoire que l'on souhaite réinitialiser. En sélectionnant 0 bit, un bit de l'un ou de l'autre ou les deux, on choisit ce que l'on souhaite réinitialiser. On peut réinitialiser :
  TCNT1 = 0;
  TIMSK1 = (1 << OCIE1A);
}


- la flash (boot0 à 0) : mémoire non volatile pour le code principal. Adresse : 0x0800 0000 - 0x0801 FFFF d'après le memory mapping (p43/142).
// ------------------ PILE ------------------ //
void init_pile(int n) {
  uint16_t savedSP = SP;
  uint16_t addr = (uint16_t)task[n].taskAddress;


- la ROM - Read Only Memory (boot0 à 1 et boot1 à 0) : mémoire non volatile que l'on change rarement sauf si besoin de changer mode communication par exemple (passage en spi, uart ...). Adresse : 0x1FFF 0000 - 0x1FFF 77FF.
  SP = task[n].stackPointer;


- la SRAM - Static Random Access Memory (boot0 à 1 et boot1 à 1) : mémoire volatile pour le débogage. Adresse : 0x2000 0000 - 0x2000 7FFF.
  // PC (low puis high)
  asm volatile("push %A0" ::"r"(addr));
  asm volatile("push %B0" ::"r"(addr));


On a aussi le pin NRST (Not Reset car actif à l'état bas) pour réinitialiser le microcontrôleur.
  // r0-r31
  for (int i = 0; i < 32; i++)
    asm volatile("push __zero_reg__");


====== Switchs ======
  // SREG = I=1
On a 3 switchs qui peuvent servir pour choisir les modes de boot ?. [PRECISER UTILITE]
  uint8_t s = 0x80;
  asm volatile("push %0" ::"r"(s));


====== Communication ======
  task[n].stackPointer = SP;
On a prévu différents types de communications selon les utilisations : SPI pour les cartes filles mais aussi UART et I2C amélioré si besoin pour une potentielle carte FPGA.
  SP = savedSP;
}


====== Cartes filles ======
// ------------------ SCHEDULER ------------------ //
On a prévu de la place pour 5 cartes filles, sans compter la carte mémoire et la carte FPGA potentielle.
void scheduler(void) {
  currentTask = (currentTask + 1) % nbTasks;
}


====== Leds ======
// ------------------ ISR TIMER1 ------------------ //
3 leds supplémentaires ont étés ajoutées pour différents tests, utile pour l'ordonnanceur potentiellement par exemple.
ISR(TIMER1_COMPA_vect, ISR_NAKED) {


====== JTAG et SWD ======
  /* Sauvegarde du contexte de la tâche interrompue */
Le bloc JTAG sert pour le débogage de la carte. Ici sur le stm32, c'est combiné à un deuxième mode de débogage spécifique aux stm32 : le SWD, une version simplifiée de JTAG.
  SAVE_REGISTERS();
  task[currentTask].stackPointer = SP;


JTAG (Joint Test Action Group) :
  /* Appel à l'ordonnanceur qui choisi la prochaine tache */
  scheduler();


* TCK : Test Clock, l'horloge du JTAG
  /* Récupération du contexte de la tâche ré-activée */
* TMS : Test Mode Select
  SP = task[currentTask].stackPointer;
* TDI : Test Data In pour envoyer des données depuis le JTAG au microcontrôleur.
  RESTORE_REGISTERS();
* TDO : Test Data Out pour envoyer des données depuis le microcontrôleur au JTAG.
* RTCK : Return Test Clock


SWD (Serial Wire Debug) :
  asm volatile("reti");
}


* SWCLK : comme TCK
</syntaxhighlight>Nous avons ici un ordonnanceur préemptif : une fois que le minuteur a atteint son nombre de "ticks", il appelle l'ISR : Interrupt Service Routine qui va :
* SWDIO : comme TMS
* Sauvegarder les registres grâce à la fonction assembleur SAVE_REGISTERS définie dans ordonnanceur.h, qui permet de sauvegarder les registres de la tâche interrompue
* SWO : comme TDO
* Appeller l'ordonnanceur qui va faire la bascule des tâches.
* Restorer le contexte, et tous les registres de la tâche que l'on va exécuter.
<syntaxhighlight lang="c">
ISR(TIMER1_COMPA_vect, ISR_NAKED){
   
    /* Sauvegarde du contexte de la tâche interrompue */
    SAVE_REGISTERS();
    task[currentTask].stackPointer = SP;


===== Cartes filles =====
    /* Appel à l'ordonnanceur qui choisi la prochaine tache */
Notre carte mère peut acceuillir 5 cartes filles communicantes en SPI parmi lesquelles :
    scheduler();


* carte clavier
    /* Récupération du contexte de la tâche ré-activée */
* carte écran
    SP = task[currentTask].stackPointer;
* carte réseau
    RESTORE_REGISTERS();
* carte son
* une autre carte


Et en plus de cela, on a aussi la carte "fille" pour la gestion de la mémoire = le boîtier SD (en SPI également) ainsi que la carte fille FPGA ou d'autre cartes qui peuvent communiquer en UART ou I2C amélioré (car SMBA).[[Fichier:Mere schematique.pdf|left|600px|alt=Mere schematique|vignette|Mere schematique]]
    asm volatile("reti");
<p style="clear: both;" />Remarque : pour voir les différentes pages de la schématique, cliquer sur le fichier.
}
</syntaxhighlight>L'ordonnanceur est ici round-robin, il exécute donc les tâches les unes après les autres sans priorité sous sa forme la plus minimaliste.
====== MAIN ======
Dans le fichier main.c, voici comment les librairies vues ensemble précedemment sont appelées :<syntaxhighlight lang="c">
#include <avr/interrupt.h>
#include <util/delay.h>


==== Vue 3D ====
#include "./lib/HARDWARE/hardware.h"
[[Fichier:Carte mere 3D.png|left|650px|alt=Carte mere 3D|vignette|Carte mere 3D]]
#include "./lib/ORDONNANCEUR/ordonnanceur.h"
[[Fichier:Carte mere 3D backside.png|right|650px|alt=Carte mere 3D backside|vignette|Carte mere 3D backside]]
<p style="clear: both;" />


==== Brasure ====
#include "./lib/TASK/task.h"
INSERER PHOTO
#include "./lib/USART/usart.h"
<p style="clear: both;" />


=== Software ===
int main(void) {
  cli(); // désactiver interruptions


==== NUCLEO-F410RB ====
  setupHardware();
En attendant de recevoir notre carte mère et afin de prendre en main la programmation quelque peu spécifique des arm, on s'entraîne sur la carte NUCLEO-F410RB qui possède le même microcontrôleur STM32F410R8T6 que celui de notre carte mère.


===== Spécifications =====
  // initialisation des piles
La carte NUCLEO-F410RB est "séparée" en deux parties :
  init_tasks();


* la partie haute de la carte : c'est le programmateur spécifique à STM32 nommé ST-LINK qui permet de programmer :
  // TIMER1 config à 20 ms
* la partie basse de la carte : qui est elle dédiée à l'utilisateur.
  init_timer1(64, 20);


===== STM32CubeIDE =====
  // charger la pile de la premi??re t??che
  SP = task[0].stackPointer;
  RESTORE_REGISTERS();


====== Test led ======
  asm volatile("reti");
Pour commencer à comprendre, on utilise dans un premier temps l'IDE STM32CubeIDE dont l'on se passera ensuite afin d'être proche du matériel et de ne pas passer par plein de librairies.


Dans notre fichier nucleo.ioc, on a toutes les spécifications de notre carte dont le pinout.
  return 0;
 
}
Premier programme test : on fait clignoter la led LD2. On voit sur le fichier nucleo.ioc que LD2 est sur le pin PA5 du microcontrôleur.


En allant dans l'arborescence à gauche, on va dans Core -> Src -> main.c.
On ouvre le main et dans la boucle while, on va faire clignoter notre led D2 grâce à la librairie HAL (Hardware Abstraction Layer) dont l'on se passera par la suite (c'est juste pour les premiers tests).<syntaxhighlight lang="c">
while (1)
  {
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); //change d'état
  HAL_Delay(500); //attend 500ms
  }
</syntaxhighlight>
</syntaxhighlight>
<p style="clear: both;" />


[[Fichier:Nucleo clignote.mp4|center|500px|vignette|nucleo_clignote]]
==Carte mère==
<p style="clear: both;" />
La deuxième carte à réaliser est la carte mère avec une spécificité cependant, à savoir une puce STM32F410R8T6 en tant que microcontrôleur.


===== Sans IDE =====
Remarque : M. Redon a également fait une carte mère mais basée sur un ATSAMD21G8A-A sur notre Git.
=== Hardware ===


====== Test led ======
==== Microprocesseur ====
Après avoir testé le code dans l'IDE, on va maintenant coder avec moins de librairies subsidiaires avec juste les fichiers utiles pour la puce STM32F410RB que l'on va chercher sur le site de STMicroelectronics .
On utilise la puce STM32F410R8T6 basée sur un Cortex-M4.


On va dans la catégorie Tools et software : https://www.st.com/en/microcontrollers-microprocessors/stm32f410rb.html#tools-software.
===== Signification du nom de la puce =====
Signification (cf p 134 de la short datasheet) :


On pourrait sélectionner directement l'evaluation tools de la nucleo mais ce n'est pas l'objectif ici puisque la nucleo nous permet juste de s'entraîner sur le microcontrôleur F410 le temps que l'on recoive notre carte. Donc on va plutôt télécharger STM32CubeF4.
Arm based 32-bit microcontroller


Suite à cela, on ajoutera différents fichiers :
* F = General-purpose
* R = 64 pins
* 8 = 64 Kbytes of Flash memory
* T = package LQFP
* 6 = Industrial temperature range, - 40 to 85 °C


# stm32cubef4-v1-28-3 : le firmware de STMicroelectronics
===== Datasheets =====
# main.c : code principal
Datasheet de la puce  STM32F410R8T6 :
# main.h
[[Fichier:STM32 datasheet.pdf|left|400px|alt=STM32_datasheet|vignette|STM32_datasheet]]
# ordonnanceur.c : uniquement pour l'ordonnanceur, le main.c gère le reste, les différents fichiers et variables à inclure
<p style="clear: both;" />On peut également retrouver des pages supplémentaires afin de bien dimensionner notre quartz et les capacités aux alentours.
# ordonnanceur.h
# un makefile  : pour compiler notre projet et les fichiers du firmware. On compile avec arm-none-eabi-gcc. On rajoute les chemins d'accès nécéssaires à la compilation du main.h qui a besoin du fichier stm32f410rx.h. On a également besoin du fichier core_cm4.h, d'où le second chemin :  INCLUDES = -Istm32cubef4-v1-28-3/STM32Cube_FW_F4_V1.28.0/Drivers/CMSIS/Device/ST/STM32F4xx/Include \ -Istm32cubef4-v1-28-3/STM32Cube_FW_F4_V1.28.0/Drivers/CMSIS/Include
# un fichier nommé linker.ld : à la différence des atmega, notre microcontrôleur a une organisation mémoire plus complexe donc ce fichier nous permet de choisir où placer chaque type de données (cf les sections elf) dans la mémoire du microcontrôleur. On y précise la taille de la flash : 128ko et la taille de la RAM : 32 ko ainsi que leurs adresses trouvées page 40/763 de la datasheet. On y initialise également les sections .text (le code), .data (variables initialisées) et .bss (variables non initialisées).
# un fichier startup_stm32f410rx.s : un fichier assembleur qui gère les interruptions, les resets, l'initialisation des données que l'on copie colle depuis /stm32/stm32cubef4-v1-28-3/STM32Cube_FW_F4_V1.28.0/Drivers/CMSIS/Device/ST/STM32F4xx/Source/Templates/gcc. Remarque sur ce qu'est thumb : sur notre stm32, on utilise ARM et thumb 2 dont le rôle est d'avoir un code plus compact en mélangeant des instructions à la fois de 16 et 32 bits.
<p style="clear: both;" />
Remarque :  commande à taper quand la carte "fail to connect" : <syntaxhighlight lang="c">
st-flash --connect-under-reset erase
</syntaxhighlight>
<p style="clear: both;" />


====== Ordonnanceur ======
Document AN2867- Guidelines for oscillator design on STM8AF/AL/S and STM32 MCUs/MPUs à retrouver sur le lien suivant : https://www.st.com/en/microcontrollers-microprocessors/stm32f410/documentation.html
<p style="clear: both;" />


Il faut également faire l'ordonnanceur que l'on doit maintenant adapter non plus au shield et aux avr mais à la carte mère et aux arm. Notre ordonnanceur était originellement composé uniquement des fichiers ordonnanceur.c et ordonnanceur.h mais on a implémenté pas mal de fonctions donc on place celles-ci dans un nouveau fichier processus.c accompagné de son processus.h. Ci-dessous la description du contenu de ces 4 fichiers.
Toutes les datasheets ayant servi pour la creation du Kicad de la carte se trouve dans le dossier Datasheet.
* processus.h :
==== Schématique ====
Il inclut les librairies nécéssaires et les prototypes des fonctions implémentées dans processus.c
Notre schématique est composée de quatres sous feuilles, respectivement pour l'alimentation, le microcontrôleur, la carte mémoire et les cartes filles.
* processus.c :


# Fonction Delay : la fonction delay n'est pas présente sous arm donc on l'implémente ici.<syntaxhighlight lang="c">
===== Alimentation =====
void delay(volatile uint32_t t){
L'alimentation se fait via du 5V et est ensuite directement convertie en 3,3V par le biais du régulateur afin d'alimenter le microcontrôleur. C'est pourquoi les bus de données USB ne sont pas utilisées car l'USB servira ici uniquement à l'alimentation et pas à la transmission de données. On ajouté également des mounting holes pour fixer la carte.
    while (t--){
        __asm__("nop");
    }
}
</syntaxhighlight>
# Tâches pour la led : ci-dessous des fonctions permettant d'allumer, éteindre et faire clignoter la led LD2 de la nucleo. <syntaxhighlight lang="c">
void init_led2(void){
    //active l’horloge pour GPIOA car ds les stm32, les peripheriques sont eteints pr economiser de l energie et on doit appeler la clock pr réveiller les gpio
    RCC->AHB1ENR |= (1 << 0);
    //on veut écrire 01 (état haut) et pas 11 (analogique) pour les broches 10 et 11 donc on doit procéder en deux étapes : d'abord effacer puis mettre en sortie
    GPIOA->MODER &= ~(0x3 << (5*2));
    GPIOA->MODER |=  (0x1 << (5*2));
}


void led2_on(void){
===== Carte mémoire =====
    GPIOA->ODR |= (1 << 5);
La carte mémoire ou carte fille SD est sensiblement la même que celle pour le shield. On a juste rajouté une capacité de découplage car la carte SD va recevoir et envoyer beaucoup de données rapidement.
}


void led2_off(void){
===== Microcontrôleur =====
    GPIOA->ODR &= ~(1 << 5);
Le microcontrôleur est composé de beaucoup de broches dédiées à l'alimentation, aux horloges, aux boots, à la communication, aux cartes filles, aux switchs, aux leds et au JTAG (voir sections suivantes).
}


void led2_blink(void){
====== Alimentation ======
    while(1){
Les broches VDD servent à l'alimentation numérique et VDDA à l'alimentation analogique, ici séparée pour filtrer de manière plus précise car  plus sensible que le numérique. En effet, pour filtrer les hautes fréquences en numérique, les capacités de découplage suffisent alors qu'en analogique le signal d'entrée nécessite une gestion plus précise avec une ferrite.
        GPIOA->ODR ^= (1 << 5);
        delay(1000000);
    }
}


void led2_blink_task(void){
====== Horloges ======
    static uint32_t counter = 0;
On a ici deux horloges :
    counter++;
    if(counter >= 10){ // clignote toutes les 10 interruptions
        GPIOA->ODR ^= (1 << 5);
        counter = 0;
    }
}
</syntaxhighlight>
# Tâches pour le série : envoyer un caractère, une chaîne de caractère, recevoir et allumer une led quand l'utilisateur écrit la lettre 'a' dans le minicom.<syntaxhighlight lang="c">
void init_usart2(void){
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    //PA2 pour tx et PA3 pour rx
    GPIOA->MODER &= ~((3 << (2*2)) | (3 << (3*2)));
    GPIOA->MODER |= (2 << (2*2)) | (2 << (3*2));
    GPIOA->AFR[0] &= ~((0xF << (4*2)) | (0xF << (4*3))); //efface registre alternate function
    GPIOA->AFR[0] |= (7 << (4*2)) | (7 << (4*3)); //attribue AF7 dédié à usart2
    USART2->BRR = 0x0683; //baudrate à 9600
    USART2->CR1 = USART_CR1_TE | USART_CR1_RE; //transmit et receive enable pour tx et rx
    USART2->CR1 |= USART_CR1_UE; //usart enable
}


void usart2_send_char(char c){
* Première horloge : on peut soit choisir l'oscillateur RC de 16 MHz ou une horloge externe comprise entre 4 et 26 MHz (p18/142)  
    while (!(USART2->SR & USART_SR_TXE)); //qd le bit transmit data register empty est à 0 (donc data register possède des données)
* Deuxième horloge : horloge pour le temps réel de 32kHZ (donc pas une application qu'on vise) (p22/142)
    USART2->DR = c; //écrire le caractère
}


void usart2_send_string(char *s){
====== Boot et configuration ======
    while (*s){
Les boot 0 et 1 permettent de choisir le bloc mémoire  :
        usart2_send_char(*s++);
    }
}


char usart2_receive(void){
- la flash (boot0 à 0) : mémoire non volatile pour le code principal. Adresse : 0x0800 0000 - 0x0801 FFFF d'après le memory mapping (p43/142).
    while (!(USART2->SR & USART_SR_RXNE)); //qd caractère
    return (char)(USART2->DR & 0xFF); //char sur 16bits on garde que bits de données
}


void led2_blink_serial(void){
- la ROM - Read Only Memory (boot0 à 1 et boot1 à 0) : mémoire non volatile que l'on change rarement sauf si besoin de changer mode communication par exemple (passage en spi, uart ...). Adresse : 0x1FFF 0000 - 0x1FFF 77FF.
    for (int i=0; i<10; i++){
        GPIOA->ODR ^= (1 << 5);
        delay(1000000);
    }
}


void led_serial(void){
- la SRAM - Static Random Access Memory (boot0 à 1 et boot1 à 1) : mémoire volatile pour le débogage. Adresse : 0x2000 0000 - 0x2000 7FFF.
    while(1){
        char c = usart2_receive();
        usart2_send_char(c); //echo vers Minicom
        if(c == 'a')
            led2_blink_serial();
    }
}
</syntaxhighlight>Commande minicom à taper dans le terminal (-o permet d'éviter que minicom envoie des commandes d'initialisation) :<syntaxhighlight lang="c">
sudo minicom -D /dev/ttyACM0 -b 9600 -o
</syntaxhighlight>Exemple de tâche (non ordonnancée pour le moment) où l'on affiche un message voulu, ici "heyyy" sur minicom[[Fichier:Minicom2.png|center|500px|alt=Minicom_|vignette|Minicom_]]
# Tâche avec le bouton : allumer la led LD2 quand on appuie sur le bouton user (en bleu, le noir étant dédié au reset).<syntaxhighlight lang="c">
void init_user_button(void){
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
    GPIOC->MODER &= ~(0x3 << (13 * 2)); //PC13 en entrée
    //pull up
    GPIOC->PUPDR &= ~(0x3 << (13 * 2));
    GPIOC->PUPDR |=  (0x1 << (13 * 2)); //01 : pull up
}


uint8_t user_button_pressed(void){
On a aussi le pin NRST (Not Reset car actif à l'état bas) pour réinitialiser le microcontrôleur.
    return (GPIOC->IDR & (1 << 13)) == 0; //bouton actif à 0
}


void task_button_led(void){
====== Communication ======
    while(1){
On a prévu différents types de communications selon les utilisations : SPI pour les cartes filles mais aussi UART et I2C amélioré si besoin pour une potentielle carte FPGA.
        if(user_button_pressed()){
            led2_on();
        } else {
            led2_off();
        }
    }
}


</syntaxhighlight><p style="clear: both;" />
====== Cartes filles ======
* ordonnanceur.h :
On a prévu de la place pour 5 cartes filles, sans compter la carte mémoire et la carte FPGA potentielle.


On y ajoute les librairies, processus.h et la liste des fonctions implémentées dans ordonnanceur.c.
====== Leds ======
3 leds supplémentaires ont étés ajoutées pour différents tests, utile pour tester en premier lieu le microcontrôleur puis l'ordonnanceur.


* ordonnanceur.c :
====== JTAG et SWD ======
Ce fichier gère le minuteur, les interruptions, la gestion des tâches.
Le bloc JTAG sert pour la programmation de la carte :


# minuteur : pour pouvoir effectuer une tâche pendant un certain temps. <syntaxhighlight lang="c">
* SWCLK : comme TCK
extern uint32_t SystemCoreClock; //freq horloge
* SWDIO : comme TMS
* SWO : comme TDO


void init_minuteur(uint16_t prescaler, uint16_t periode_ms){
===== Cartes filles =====
  RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; //active l'horloge de TIM6
Notre carte mère peut acceuillir 5 cartes filles communicantes en SPI parmi lesquelles :  
  TIM6->PSC = prescaler - 1; //prescaler
  uint32_t tick_freq = SystemCoreClock/prescaler; //période
  //TIM6->ARR = (tick_freq * periode_ms)/1000-1; //Auto-Reload Register est la période avant que le timer fasse une interruption. (tick_freq * periode_ms)/1000 correspond au nb de ticks durant cette période.
  TIM6->ARR = (uint32_t)(((uint64_t)tick_freq * periode_ms)/1000 - 1);
  TIM6->CNT = 0; //reset compteur
  TIM6->DIER |= TIM_DIER_UIE; //interruption activée quand compteur atteint nb de ticks de ARR
  TIM6->CR1 |= TIM_CR1_CEN; //compteur
  NVIC_SetPriority(TIM6_DAC_IRQn, 0x1); //prio haute
  NVIC_EnableIRQ(TIM6_DAC_IRQn); //active l'interruption dans le NVIC (Nested Vector Interrupt Controller)
}
</syntaxhighlight>
On ajoute ensuite le shield sur la nucleo afin de pouvoir tester l'ordonnanceur avec plus de tâches puisque la nucleo seule possède très peu de leds.[[Fichier:Nucleo ordonnanceur.jpg|center|500px|alt=nucleo_ordonnanceur|vignette|nucleo_ordonnanceur]]
====== Système de fichier ======
Afin de débuter le système de fichiers, on connecte notre mémoire (la carte SD) à notre nucleo.[[Fichier:Nulceo sd.jpg|center|500px|alt=nucleo_sd|vignette|nucleo_sd]]
<p style="clear: both;" />


'''Test d'initialisation'''
* carte clavier
* carte écran
* carte réseau
* carte son
* une autre carte


On commence par initialiser notre carte SD grâce à fichier.c et en premier lieu cela ne fonctionnait pas on avait status=1 et erreur=8 (et en conséquent une taille nulle puisqu'il n'arrive pas à initialiser la carte). La carte n'était pas de bonne qualité et ne communiquait pas en SPI mais sans doute avec un autre protocole. Mais par la suite avec la carte SD donnée par M. Redon, on a réussi à la phase d'initialisation.  
Et en plus de cela, on a aussi la carte "fille" pour la gestion de la mémoire = le boîtier SD (en SPI également) ainsi que la carte fille FPGA ou d'autre cartes qui peuvent communiquer en UART ou I2C amélioré (car SMBA).[[Fichier:Mere schematique.pdf|left|600px|alt=Mere schematique|vignette|Mere schematique]]
<p style="clear: both;" />
==== Vue 3D ====
[[Fichier:Carte mere 3D.png|left|650px|alt=Carte mere 3D|vignette|Carte mere 3D]]
[[Fichier:Carte mere 3D backside.png|right|650px|alt=Carte mere 3D backside|vignette|Carte mere 3D backside]]
<p style="clear: both;" />


On obtient alors :
==== Brasure ====
INSERER PHOTO


* son type, ici 2 (l'autre type étant 1 pour les micro cartes sd moins performantes)
=== Software ===
* ainsi que sa taille : 3 911 860 secteurs. Un secteur étant de 512 octets, on retrouve bien la taille écrite sur notre carte à savoir 2Gb.


[[Fichier:Sd init.png|center|600px|alt=Sd init|vignette|Sd init]]
==== NUCLEO-F410RB ====
<p style="clear: both;" />
En attendant de recevoir notre carte mère et afin de prendre en main la programmation quelque peu spécifique des arm, on s'entraîne sur la carte NUCLEO-F410RB qui possède le même microcontrôleur STM32F410R8T6 que celui de notre carte mère.


Suite à cela, on teste l'écriture, qui s'avère opérationelle (status = 1) puis l'écriture, elle aussi fonctionnelle puisque l'on affiche bien les 3 "blocs" voulus.
===== Spécifications =====
La carte NUCLEO-F410RB est "séparée" en deux parties :


[[Fichier:Sd read test.png|600px|alt=Sd read test|vignette|Sd read test]]
* la partie haute de la carte : c'est le programmateur spécifique à STM32 nommé ST-LINK qui permet de programmer :
[[Fichier:Sd write test.png|600px|alt=Sd write test|vignette|Sd write test|gauche]]
* la partie basse de la carte : qui est elle dédiée à l'utilisateur.
<p style="clear: both;" />'''Commandes pour le système de fichier'''


La prochaine étape est de coder les commandes nécéssaires telles que append, read, remove, rename, copy.
===== STM32CubeIDE =====


# format : efface l'entièreté du système de fichier.
====== Test led ======
# list : liste les noms des fichiers contenus dans le système de fichier, équivalent du "ls" sous linux.
Pour commencer à comprendre, on utilise dans un premier temps l'IDE STM32CubeIDE dont l'on se passera ensuite afin d'être proche du matériel et de ne pas passer par plein de librairies.
# append : créé un fichier si non existant et ajoute du texte si le fichier existe déjà. Commande de la forme append fichier/données. Combine le "touch" (create dans l'énoncé) et l'écriture de données.
 
# read : permet de lire le contenu d'un fichier, équivalent du "cat".
Dans notre fichier nucleo.ioc, on a toutes les spécifications de notre carte dont le pinout.
# remove : supprime le fichier en paramètre, équivalent du "rm".
 
# rename : renommer un fichier, équivalent du "mv" en moins puissant.
Premier programme test : on fait clignoter la led LD2. On voit sur le fichier nucleo.ioc que LD2 est sur le pin PA5 du microcontrôleur.
# copy : copie un fichier et ses données dans un second fichier, équivalent du "cp".


[[Fichier:Demo fileSystem.webm|center|500px|vignette|demo_fileSystem]]
En allant dans l'arborescence à gauche, on va dans Core -> Src -> main.c.
On ouvre le main et dans la boucle while, on va faire clignoter notre led D2 grâce à la librairie HAL (Hardware Abstraction Layer) dont l'on se passera par la suite (c'est juste pour les premiers tests).<syntaxhighlight lang="c">
while (1)
  {
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); //change d'état
  HAL_Delay(500); //attend 500ms
  }
</syntaxhighlight>
<p style="clear: both;" />
<p style="clear: both;" />


==== Notre carte mère ====
[[Fichier:Nucleo clignote.mp4|center|500px|vignette|nucleo_clignote]]
<p style="clear: both;" />C'est un peu l'équivalent arduino, on code avec l'abstraction matérielle. Nous allons par la suite procéder au niveau bare metal.
 
===== Bare metal =====
Ce chapitre présente la programmation Bare Metal sur microcontrôleurs ARM, en utilisant un STM32 NUCLEO-F410RB comme exemple. Nous aborderons le startup, le linker, les exemples de clignotement LED, l'utilisation de CMSIS, ainsi que la gestion de périphériques comme le SPI, l'UART, etc...


===== Test led =====
====== Startup (.s) ======
Afin de vérifier que notre PCB reçu fonctionne, on teste notre carte en faisant clignoter une led.<syntaxhighlight lang="c">
Le Startup file est crucial en Bare Metal. Une erreur dans ce fichier peut empêcher le microcontrôleur de démarrer ou provoquer des crashs.
#define LED_PIN 9
Rôles principaux :


int main(void) {
- Définir l'environnement nécessaire à l'exécution de main().
  // Activer horloge GPIOC
 
  RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOCEN_Pos);
- S'exécuter avant main() et lancer ensuite main().
 
- Être adapté à la target (processeur) utilisée.
 
- Placer correctement la table des vecteurs comme exigé par les ARM Cortex-M.


  GPIOC->MODER &= ~(0x3 << (LED_PIN * 2)); // Clear
- Initialiser la pile correctement.
  GPIOC->MODER |= 0x1 << (LED_PIN * 2);    // Output
  GPIOC->OTYPER &= ~(1 << LED_PIN);        // Push-pull
  GPIOC->PUPDR &= ~(0x3 << (LED_PIN * 2)); // No pull


  while (1) {
- Initialiser les sections .data et .bss dans la SRAM.
    GPIOC->ODR ^= (1 << LED_PIN);
    for (volatile uint32_t i = 0; i < 1000000; i++)
      ;
  }
}


</syntaxhighlight>
====== Linker (.ld) ======
Le Linker Script détermine comment les sections du code sont placées en mémoire.
''Fonctionnalités :''
* Définir les adresses absolues des sections.
* Définir les zones mémoire, leurs tailles et adresses.
* Fournir les instructions au linker GNU via l'option -T.
* L'extension de fichier est .ld.


===== Test écran =====
Le code linker sert à guider le compilateur pour assembler toutes les sections d’un programme en un fichier binaire unique.  Sur un microcontrôleur comme le '''STM32F410RB''', il faut indiquer où chaque partie du code et des données doit être placée dans la '''mémoire du MCU :'''
L'idée est d'afficher un compteur sur un afficheur 7 segments, notre "carte écran" : https://gitea.plil.fr/ahouduss/SE4-Pico-B6/src/branch/master/03-Code/Carte_mere/Notre_PCB/02-Compteur.


Notre test écran est structuré de la manière suivante :
- Où commence le code executable, son point d'entrée ;
* un main.c
* un dossier pour la carte écran
* un dossier hardware_setup
* un dossier pour le spi


Description des fichiers et fonctions implémentées :
- Définit chaque region de la memoire qui existe sur le microcontrôleur et leur taille ;


====== hardware_setup ======
- Où placer les sections differentes du code en memoire (exemple : interruption, etc...).
Dans l'idée on peut le faire à la main afin de mieux assimiler le linker puis récuperer celui générer automatiquement par le logiciel STM32IDE (également disponible sur github) afin de ne pas avoir a y retoucher a chaque fois et ne pas briquer accidentellement notre puce.
Pour realiser un linker on doit preciser la SRAM et sa flash. Cette partie décrit les zones mémoire physiques du STM32F410RB. Il faut se fier au plan memoire de la datasheet p.40/763
Sur celui-ci nous constatons que SRAM est à 32kB et commence à 0x2000 0000 et que la fash commence à 0x0800 0000 et sa taille est de 128kB (donné à la figure 1 page 37/763).


On reprend notre fonction setupPin que l'on adapte à arm afin d'initialiser les pins de notre carte :<syntaxhighlight lang="c">
On peut alors placer dans le linker :<syntaxhighlight lang="c">
#define OFFSET_BSRR_OFF 16
MEMORY
{
  FLASH (rx): ORIGIN = 0x08000000, LENGTH = 128K
  SRAM (rwx): ORIGIN = 0x20000000, LENGTH = 32K
}
</syntaxhighlight>(rx) → read et execute, donc on peut lire et exécuter du code dedans.
(rwx) → read, write, execute (en pratique, on n’exécute pas depuis la RAM, mais certains MCU le permettent).
'''<u>Sections</u>'''Chaque programme compilé contient plusieurs sections générées par le compilateur. L'ordre à laquelle ils sont cris découle de la convention utilisé dans CMSIS (fichier startup fournis par ARM).


#define MODER_CLEAR 0x3
#define MODER_NumberBitParPin 0x2


#define PUPDR_CLEAR 0x3
Tous les compilateurs suivent la même idée :
#define PUPDR_NumberBitParPin 0x2


#define OTYPER_CLEAR 0x1
- Sections nécessaires au CPU d’abord (vecteurs, code)


typedef enum {
- Sections de données ensuite (initialisées, non initialisées)
  INPUT,
 
  OUTPUT,
- Mémoire dynamique à la fin (heap, stack)
  ALTERNATE_FUNCTION,
La '''partie ISR_VECTOR''' indique les vecteurs d'interruptions :<syntaxhighlight lang="c">
  ANALOG,
isr_vector :
} portModeRegister;
{
    KEEP(*(.isr_vector))
} >FLASH


typedef enum {
</syntaxhighlight>".isr_vector" : C'est la section qui contient le vecteur d'interruptions (ISR - Interrupt Service Routine). Les vecteurs d'interruptions sont des adresses de fonction qui seront appelées lorsque des interruptions spécifiques se produisent.
  NO_PULL,
"KEEP" : Cette directive indique au linker de conserver cette section dans le binaire final, même si elle semble inutilisée par le programme. Cela est crucial pour les vecteurs d'interruptions qui doivent absolument être présents dans le binaire.
   PULL_UP,
">FLASH" : Cela indique que cette section doit être placée en mémoire Flash, qui est généralement de la mémoire non-volatile, utilisée pour stocker le programme.
   PULL_DOWN,
Elle est placée en Flash pour être disponible dès le reset.
} portPullUpPullDownRegister;
La '''partie TEXT''' contient le code de notre programme :<syntaxhighlight lang="c">
.text :
   {
    . = ALIGN(4);
    *(.text)
    *(.rodata)
    . = ALIGN(4);
    _etext = .;
   } >FLASH
</syntaxhighlight>.text = code exécutable (fonctions, instructions machine).
.rodata = données constantes (const int, chaînes de caractères, etc.).


void setupPin(GPIO_TypeDef *GPIOx, uint8_t PINx, portModeRegister portMode) {
_etext marque la fin de la zone code, utile pour copier ensuite la section .data au démarrage.
  // ACTIVATION DE L'HORLOGE GPIO SPECIFIQUE
ALIGN(4) permet d'aligner l'adresse courante sur une frontière de 4 octets. Cela garantit que le code est correctement aligné en mémoire, ce qui peut être nécessaire pour des performances optimales ou des restrictions matérielles.
   uint32_t RCC_AHB1ENR_GPIOxEN_Pos;
La '''partie DATA''' contient les variables initialisées :<syntaxhighlight lang="c">
.data :
   {
    . = ALIGN(4);
    _sdata = .;
    *(.data)


  if (GPIOx == GPIOA)
     . = ALIGN(4);
     RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOAEN_Pos;
     _edata = .;
  else if (GPIOx == GPIOB)
   } >SRAM AT> FLASH
    RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOBEN_Pos;
  else if (GPIOx == GPIOC)
     RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOCEN_Pos;
   else
    return; // GPIO non existant sur ce microcontroleur


  if (!(RCC->AHB1ENR & (1 << RCC_AHB1ENR_GPIOxEN_Pos)))
</syntaxhighlight>.data contient les variables globales initialisées (ex: int x = 5;).
    RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOxEN_Pos);
Ces valeurs sont stockées dans la Flash au moment de la programmation, mais copiées dans la RAM lors du démarrage (d’où >SRAM AT>FLASH).


   // CLEAR AVANT MODIFICATION
_sdata et _edata servent au code d’initialisation (startup.s) pour savoir quoi copier et combien d’octets.
   // On clear après l'activation d'horloge !
La partie BSS contient les variables non initialisées :<syntaxhighlight lang="c">
  GPIOx->MODER &= ~(MODER_CLEAR << (PINx * MODER_NumberBitParPin));
   .bss :
  GPIOx->PUPDR &= ~(PUPDR_CLEAR << (PINx * PUPDR_NumberBitParPin));
   {
   GPIOx->OTYPER &= ~(OTYPER_CLEAR << PINx); // Push-pull
    . = ALIGN(4);
    _sbss = .;
    *(.bss)
    . = ALIGN(4);
    _ebss = .;
   } >SRAM
</syntaxhighlight>.bss contient les variables globales non initialisées (ex: int counter;).
Ces variables ne sont pas stockées en Flash, car elles ne contiennent pas de valeur initiale.


  // TYPE DE PORT (Input, Output, Alternative, Analogique)
Au démarrage, le startup code remplit cette zone avec des zéros (memset), d’où “zeroed during startup”.
  portPullUpPullDownRegister typePull = NO_PULL;
Et voici le code complet du linker : <syntaxhighlight lang="c">
  uint8_t optionPort = 0x00;
MEMORY{
    FLASH (rx): ORIGIN = 0x08000000, LENGTH = 128K
    SRAM (rwx): ORIGIN = 0x20000000, LENGTH = 32K
}


  switch (portMode) {
SECTIONS
   case INPUT:
{
    optionPort = 0x00;
   .isr_vector :
     typePull = PULL_UP;
  {
    break;
     KEEP(*(.isr_vector))
  } >FLASH


   case OUTPUT:
   .text :
     optionPort = 0x01;
  {
     typePull = NO_PULL;
    . = ALIGN(4);
    *(.text)
    *(.rodata)
     . = ALIGN(4);
     _etext = .;
  } >FLASH


     if (1)
  .data :
      GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
  {
     else
     . = ALIGN(4);
      GPIOx->OTYPER |= (1 << PINx); // Open-drain
    _sdata = .;
     *(.data)


     break;
     . = ALIGN(4);
    _edata = .;
  } >SRAM AT> FLASH


   case ALTERNATE_FUNCTION:
   .bss :
     optionPort = 0x02;
  {
     . = ALIGN(4);
    _sbss = .;
    *(.bss)
    . = ALIGN(4);
    _ebss = .;
  } >SRAM
}
 
</syntaxhighlight>Il a été tester sur ce main.c :<syntaxhighlight lang="c">
#include "../01-lib/stm32f410rx.h"
#include <stdint.h>


    if (1) {
#define LED_PIN 5
      typePull = NO_PULL;
      GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
      GPIOx->OSPEEDR |= 11 << (PINx * 2);
      // Very high speed => Cf STM32F410 datasheet tableau p95/142
    }


    break;
int main(void) {
  // Activer horloge GPIOA
  RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOAEN_Pos);


   case ANALOG:
   // do two dummy reads after enabling the peripheral clock, as per the errata
    optionPort = 0x03;
  volatile uint32_t dummy;
    break;
  dummy = RCC->AHB1ENR;
  dummy = RCC->AHB1ENR;


   default:
   GPIOA->MODER &= ~(0x3 << (LED_PIN * 2)); // Clear
     optionPort = 0x00;
  GPIOA->MODER |= 0x1 << (LED_PIN * 2);    // Output
     typePull = NO_PULL;
  GPIOA->OTYPER &= ~(1 << LED_PIN);        // Push-pull
    break;
  GPIOA->PUPDR &= ~(0x3 << (LED_PIN * 2)); // No pull
  while (1) {
     GPIOA->ODR ^= (1 << LED_PIN);
     for (volatile uint32_t i = 0; i < 1000000; i++)
      ;
   }
   }
}
</syntaxhighlight>Et le makefile :<syntaxhighlight lang="makefile">
# Compilateur et options
CC = arm-none-eabi-gcc


  // Ecriture
CFLAGS = -mcpu=cortex-m4 -mthumb -Wall -Wextra
  GPIOx->MODER |= optionPort << (PINx * MODER_NumberBitParPin);
CPPFLAGS = -DSTM32F410Rx \
-I../01-lib/gcc \
-I../01-lib/Core \
-I../01-lib
 
# Linker
LINKER_FILE = faisALaMainLinker.ld
LDFLAGS = -T $(LINKER_FILE)


  // TYPE DE PULL (No pull, Pull Down, Pull Up)
# Fichiers objets
  uint8_t optionPull = 0x00;
OBJS = main.o system_stm32f4xx.o startup_stm32f410rx.o


  // Ecriture type pull
# Cible principale
  switch (typePull) {
all: main.elf
  case NO_PULL:
 
    optionPull = 0x00;
main.elf: $(OBJS)
    break;
$(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) -lc -lm -lnosys -o $@
  case PULL_UP:
 
    optionPull = 0x01;
# Compilation des fichiers C
    break;
main.o: main.c
  case PULL_DOWN:
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
    optionPull = 0x02;
    break;
  default:
    optionPull = 0x00;
    break;
  }


  // Ecriture
system_stm32f4xx.o: ../00-cmsis-device-f4-master/Source/Templates/system_stm32f4xx.c
  GPIOx->PUPDR |= optionPull << (PINx * PUPDR_NumberBitParPin);
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
}
</syntaxhighlight>On implémente également des fonctions qui permettent d'activer, de désactiver ou de faire clignoter (une led en l'occurence) des pins.<syntaxhighlight lang="c">
void onPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  GPIOx->BSRR = (1 << PINx); // set
}


void offPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
# Assemblage du startup
  GPIOx->BSRR = (1 << (PINx + OFFSET_BSRR_OFF)); // reset
startup_stm32f410rx.o: ../01-lib/gcc/startup_stm32f410rx.s
}
arm-none-eabi-as -mcpu=cortex-m4 -mthumb $< -o $@


void togglePin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
# Converti ELF en BIN
  // Après sa lecture, le registre BSRR reset à 0 automatiquement
firmware.bin: main.elf
  if (GPIOx->ODR & (1 << PINx)) // Lecture pin, si 1 alors eteindre
arm-none-eabi-objcopy -O binary $< $@
    offPin(GPIOx, PINx);
  else // Sinon allumer
    onPin(GPIOx, PINx);
}
</syntaxhighlight>On possède également une fonction qui initialise notre carte mère, en activant les différents ports des cartes filles, le spi, les leds et les boutons.<syntaxhighlight lang="c">
void setupCarte() {
  // 1
  setupPin(GPIOC, 0, OUTPUT); // PC0, CS1
  setupPin(GPIOC, 1, OUTPUT); // PC1, RST1
  setupPin(GPIOC, 2, OUTPUT); // PC2, INT1


  // 2
# Upload
  setupPin(GPIOA, 7, OUTPUT); // PA7, CS2
flash: firmware.bin
  setupPin(GPIOB, 1, OUTPUT); // PB1, RST2
st-flash write firmware.bin 0x8000000
  setupPin(GPIOC, 4, OUTPUT); // PC4, INT2
# Nettoyage
clean:
rm -f *.o *.elf *.bin


  // 3
size:
  setupPin(GPIOC, 9, OUTPUT); // PC9, CS3
arm-none-eabi-size main.elf
  setupPin(GPIOB, 6, OUTPUT); // PB6, RST3
  setupPin(GPIOB, 5, OUTPUT); // PB5, INT3


  // 4
</syntaxhighlight>
  setupPin(GPIOA, 2, OUTPUT); // PA2, CS4
  setupPin(GPIOA, 1, OUTPUT); // PA1, RST4
  setupPin(GPIOA, 0, OUTPUT); // PA0, INT4


  // 6
===== Processus de compilation : =====
  setupPin(GPIOA, 4, OUTPUT); // PA4, CS6
- Le linker assemble ces fichiers et les place correctement en mémoire, générant un fichier .elf.


  // FPGA
- Le fichier .elf est converti en .bin et téléversé dans le microcontrôleur via ST-LINK ou OpenOCD.
  setupPin(GPIOB, 0, OUTPUT); // PB0, CS_FPGA
[[Fichier:SchemaCompil.png|centré|vignette|425x425px|Schéma chaine compilation]]


  // LEDs
  setupPin(GPIOB, 8, OUTPUT); // PB8, LED1
  setupPin(GPIOA, 6, OUTPUT); // PA6, LED2
  setupPin(GPIOB, 7, OUTPUT); // PB7, LED3


  // BTNs
Nous retrouvons notre code principal en .c qui est compiler pour en ressortir des fichiers .o qui seront ensuite traiter dans le bloc Linker qui fera le lien entre le code et les blocs mémoires réelles. A la sortie nous aurons des .elf qui une fois dans le programmer le transformera en .bin et y sera téléversé dans le mircocontrôleur.
  setupPin(GPIOC, 12, OUTPUT); // PC12, SW_1
 
  setupPin(GPIOB, 11, INPUT);  // PB11, SW_2
====== CMSIS ======
  setupPin(GPIOC, 10, INPUT);  // PC10, SW_3
C'est à ce lien : <nowiki>https://github.com/STMicroelectronics/cmsis-device-f4</nowiki> qu'on vient télécharger la bibliothèque CMSIS (Cortex Microcontroller Software Interface Standard).


  // Test LEDs allumé
CMSIS (Cortex Microcontroller Software Interface Standard) simplifie l'accès au matériel, l'équivalent d'un "#include <avr/io.h>" pour ARM.
  offPin(GPIOC, 0); // PC0, CS1
  offPin(GPIOB, 1); // PA7, CS2
  offPin(GPIOC, 9); // PC9, CS3
  offPin(GPIOA, 2); // PA2, CS4
  offPin(GPIOB, 0); // PB0, CS_FPGA


  onPin(GPIOC, 12); // PB0, CS_FPGA
C'est une bibliothèque fournie par ARM et ST, qui simplifie l'accès au matériel en proposant :
  // Setup SPI => MOSI, MISO et SCK
  spiInit();


  // Ecran
- Des définitions pour tous les registres du MCU
  ecran_init();
}
</syntaxhighlight>


====== spi ======
- Les prototypes des fonctions système (comme SystemInit())
On initialise également les fonctions pour le SPI, à retrouver ici : https://gitea.plil.fr/ahouduss/SE4-Pico-B6/src/branch/master/03-Code/Carte_mere/Notre_PCB/02-Compteur/lib/SPI.


On implémente les fonctions suivantes, commentées en détail dans le spi.c :<syntaxhighlight lang="c">
- La table des vecteurs d'interruptions (startup code)
void spiInit();
void spi_cs_on(GPIO_TypeDef *GPIOx, uint8_t PINx);
void spi_cs_off(GPIO_TypeDef *GPIOx, uint8_t PINx);
void spi_write(uint8_t data, GPIO_TypeDef *CS_GPIOx, uint8_t CS_PINx);
</syntaxhighlight>


====== écran ======
Si on veut la bibliothèque contenant plus d'informations et des examples, on peut se fier à ce repertoire : https://github.com/STMicroelectronics/STM32CubeF4/tree/master.
On écrit plusieurs fonctions pour notre carte écran, qui est pour l'instant testée par le biais de l'afficheur 7 segments d'où certainess lignes commentées.  


Ces fonctions servent à :
====== Aide à la compilation avec un simple Blink ======
Récuperation des fichiers "syscall.c" et "sysmem.c" : <nowiki>https://github.com/STMicroelectronics/STM32CubeF4/tree/master/Projects/STM32F410xx-Nucleo/Templates/STM32CubeIDE/Example/User</nowiki>
Voici le Makefile correspondant :<syntaxhighlight lang="makefile">
# Compilateur et options
CC = arm-none-eabi-gcc


* initialiser l'écran
CFLAGS = -mcpu=cortex-m4 -mthumb -Wall -Wextra
* écrire dessus en activant le spi
CPPFLAGS = -DSTM32F410Rx \
* régler l'intensité de l'écran
-I../01-lib/gcc \
* nettoyer l'écran
-I../01-lib/Core \
<syntaxhighlight lang="c">
-I../01-lib/User \
// PC11, CS5
-I../01-lib
#define CS5_GPIO GPIOC
#define CS5_PIN 11


// PB9, RST5
# Linker
#define RST5_GPIO GPIOB
LINKER_FILE = generatedLinkerIDE.ld
#define RST5_PIN 9
LDFLAGS = -T $(LINKER_FILE)


// PC13, INT5
# Répertoire build et bin
#define INT5_GPIO GPIOC
BUILD = build
#define INT5_PIN 13
BIN = bin


void ecran_init() {
# Fichiers source
  setupPin(CS5_GPIO, CS5_PIN, OUTPUT);
SRCS := main.c \
  // setupPin(RST5_GPIO, RST5_PIN, OUTPUT);
      ../01-lib/User/syscalls.c \
  // setupPin(INT5_GPIO, INT5_PIN, OUTPUT);
      ../01-lib/User/sysmem.c \
      ../01-lib/system_stm32f4xx.c
# Fichier startup asm
ASM_SRCS = ../01-lib/gcc/startup_stm32f410rx.s


  onPin(CS5_GPIO, CS5_PIN); // S'active à l'etat bas
# Objets
  // offPin(RST5_GPIO, RST5_PIN); // S'active à l'etat bas
OBJS = $(patsubst %.c,$(BUILD)/%.o,$(notdir $(SRCS))) \
}
      $(patsubst %.s,$(BUILD)/%.o,$(notdir $(ASM_SRCS)))


void ecran_spi_write(uint8_t data) {
  spi_write(data, CS5_GPIO, CS5_PIN);
}


void ecran_brightness(uint8_t intensite) {
# Cible principale
  ecran_spi_write(0x7A);      // Commande "Brightness"
all: $(BUILD)/main.elf
  ecran_spi_write(intensite); // Sécurité passive 2^8-1 = 255 qui est le maximum
 
}
# Crée le dossier build/ et bin/
$(BUILD):
mkdir -p $(BUILD)
 
$(BIN):
mkdir -p $(BIN)


void ecran_clear() {
# Link
  ecran_spi_write(0x76); // Commande "Clear"
$(BUILD)/main.elf: $(BUILD) $(OBJS)
}
$(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) -lc -lm -lnosys -o $@


</syntaxhighlight>On écrit ensuite le code du compteur :<syntaxhighlight lang="c">
# Compilation des fichiers C
uint8_t isChange = 0b1111;
$(BUILD)/%.o: %.c | $(BUILD)
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@


int unite = 0;
$(BUILD)/%.o: ../01-lib/User/%.c | $(BUILD)
int decimal = 0;
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@
int centaine = 0;
int mil = 0;


void ecran_compteur() {
$(BUILD)/%.o: ../00-cmsis-device-f4-master/Source/Templates/%.c | $(BUILD)
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@


  ecran_select_digit(3);
# Assemblage du startup
  ecran_spi_write(unite); // Data
$(BUILD)/%.o: ../01-lib/gcc/%.s | $(BUILD)
arm-none-eabi-as -mcpu=cortex-m4 -mthumb $< -o $@


  if (isChange & 0b0010) {
# Converti ELF en BIN
    isChange &= ~0b0010;
$(BIN)/firmware.bin: $(BUILD)/main.elf | $(BIN)
    ecran_select_digit(2);
arm-none-eabi-objcopy -O binary $< $@
    ecran_spi_write(decimal); // Data
  }


  if (isChange & 0b0100) {
# Upload
    isChange &= ~0b0100;
flash: $(BIN)/firmware.bin
    ecran_select_digit(1);
st-flash write $(BIN)/firmware.bin 0x8000000
    ecran_spi_write(centaine); // Data
  }
# Nettoyage
clean:
rm -rf $(BUILD) $(BIN)
 
size:
arm-none-eabi-size $(BUILD)/main.elf
</syntaxhighlight>et le main.c :<syntaxhighlight lang="c">
#include "../01-lib/stm32f410rx.h"


  if (isChange & 0b1000) {
#include <stdint.h>
    isChange &= ~0b1000;
    ecran_select_digit(0);
    ecran_spi_write(mil); // Data
  }


  if (unite < 9) {
#define LED_PIN 5
    unite++;
  } else {
    isChange |= 0b0010;
    unite = 0;
    if (decimal < 9)
      decimal++;
    else {
      decimal = 0;
      isChange |= 0b0100;


      if (centaine < 9) {
int main(void) {
        centaine++;
  // Activer horloge GPIOA
      } else {
  RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOAEN_Pos);
        centaine = 0;
        isChange |= 0b1000;


        if (mil < 9) {
  GPIOA->MODER &= ~(0x3 << (LED_PIN * 2)); // Clear
          mil++;
   GPIOA->MODER |= 0x1 << (LED_PIN * 2);    // Output
        } else {
   GPIOA->OTYPER &= ~(1 << LED_PIN);       // Push-pull
          mil = 0;
   GPIOA->PUPDR &= ~(0x3 << (LED_PIN * 2)); // No pull
        }
      }
    }
   }
}
</syntaxhighlight>
 
====== main ======
<syntaxhighlight lang="c">
int main(void) {
   setupCarte();
   ecran_brightness(100);
  int j = 0;


   while (1) {
   while (1) {
     ecran_compteur();
     GPIOA->ODR ^= (1 << LED_PIN);
    j++;
     for (volatile uint32_t i = 0; i < 1000000; i++)
 
    if (j > 10) {
      j = 0;
      togglePin(GPIOB, 8); // LED1
      togglePin(GPIOA, 6); // LED2
      togglePin(GPIOB, 7); // LED3
    }
 
     for (volatile uint32_t i = 0; i < 25000; i++)
       ;
       ;
   }
   }
}
}
</syntaxhighlight>
</syntaxhighlight>Nous faisons simplement clignoter une led afin de préparer l'environnement de compilation et vérifier que tout fonctionne. Comme ça, si il y a un bug nous pourrons vite isoler cette étape.
 
 
Afin de faire clignoter une LED il faut :
1. Activer l'horloge du GPIO correspondant. Pour ce faire, nous modifions le registre RCC->AHB1ENR. (Cf datasheet RM0401 p.119/763)<syntaxhighlight lang="c">
RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIO<PORT>EN_Pos);


== Carte fille Clavier ==
</syntaxhighlight>2. Définir la direction du PIN via le registre<syntaxhighlight lang="c">
GPIO<PORT>->MODER |= 0x1 << (<PIN_NUM> * 2);


=== Hardware ===
</syntaxhighlight>"PB2" => PORT = B et PIN_NUM = 2
Ici par exemple le PIN 3 correspond au 6ieme et 7ieme bit de MODER.<syntaxhighlight lang="c">
GPIO<PORT>->MODER |= 0x1 << (<PIN_NUM> * 2);    // Output


==== Boutons utilisés ====
</syntaxhighlight>3. Définir le type de sortie (Registre OTYPER)<syntaxhighlight lang="c">
Nous voulions implémenter un clavier à touches mécaniques. Monsieur Redon avait des switchs qui convenait donc nous n'avions pas besoin d'en recommander.
GPIO<PORT>->OTYPER &= ~(0x1 << (<PIN_NUM>));    // Push pull


Les switchs sont de la marque KAILH :
</syntaxhighlight>4. Définir le type de pull (Registre PUPDR)<syntaxhighlight lang="c">
[[Fichier:Boite Kailh.jpg|alt=Boite Kailh|vignette|center|Boite Kailh Switch]]
GPIO<PORT>->PUPDR &= ~(0x3 << (<PIN_NUM> * 2)); // Forcage 00 : No pull
<p style="clear: both;" />


Petit bémol : les '''sockets hot-swap''' ne correspondaient pas à nos boutons. Comme on peut le voir sur la photo de droite, il existe '''deux types de sockets''' pour nos modules hot-swap. Nous avons donc dû commander les modèles adaptés.
</syntaxhighlight>5. Etape finale : écriture sur le pin (Registre ODR)<syntaxhighlight lang="c">
GPIO<PORT>->ODR ^= (1 << <PIN_NUM>);


[[Fichier:Kailh Hot swap socket.png|left|300px|vignette|Kailh Hot swap socket|300x300px]]
</syntaxhighlight>On peut également modifier de façon atomique (donc juste 1 bit pas tous) via le registre BSRR.<syntaxhighlight lang="c">
[[Fichier:Bouton kailh comparaison hot swap.jpg|right|vignette|300x300px|Bouton kailh comparaison hot swap socket]]
    if (GPIO<PORT>->ODR & (1 << <PIN_NUM>))
<p style="clear: both;" />
      GPIO<PORT>->BSRR = (1 << (<PIN_NUM> + 16)); // Ici 16 est l'offset à ajouter afin d'avoir le bon registre pour eteindre
    else
      GPIO<PORT>->BSRR = (1 << <PIN_NUM>);


L’intérêt de ces modules est de '''pouvoir insérer ou retirer les boutons à volonté''', sans qu’ils soient soudés directement au PCB — ce sont les sockets qui, eux, sont soudés. Cette solution présente plusieurs avantages : elle '''facilite la réutilisation des boutons''' d’un projet à un autre et '''améliore la réparabilité''' du clavier.
</syntaxhighlight>Lorsque BSRR est lu il est reset juste après et cela permet d'éviter les conflits si une interruption intervient et modifie ODR entre 2 instructions.
Après on peut également toucher à la vitesse du pin (Registre OSPEEDR) si l'on souhaite optimiser la consommation de batterie.
''Note : Remplacer <PORT> par A ou B ou C ou etc... et remplacer <PIN> par 1,2,etc.. selon ce qu'on veut activer''


==== Concevons un clavier ! ====
====== Acceder a PA2 et PA3 sur la Nucleo ======
Notre clavier doit comporter '''62 touches''', conformément au '''format standard ISO 60 %''', et sera capable d’'''assurer l’ensemble des combinaisons de touches attendues pour un clavier moderne en 2025'''.
"SB62 and SB63 must be ON, while SB13 and SB14 must be OFF." Extrait de la datasheet UM1724 : Chapitre7.10 p26/91


Afin de customiser notre clavier, on se rend sur le site [https://www.keyboard-layout-editor.com/#/ keyboard-layout-editor] .
Il faut dessouder les resistances bridges pour accéder à ces ports. Et ensuite déplacer le jumper sur U5V.


Nous pouvons partir d'un modèle de base ou alors d'un preset :
===== Système de fichier =====
[[Fichier:Site keyboard layout .png|centré|vignette|534x534px|Site keyboard layout preset ISO 60%]]


Nous nous sommes alors inspirés des '''claviers disponibles sur le marché''' afin d’adopter un placement des touches conforme aux dispositions les plus courantes.
Afin de débuter le système de fichiers, on connecte notre mémoire (la carte SD) à notre nucleo.[[Fichier:Nulceo sd.jpg|center|500px|alt=nucleo_sd|vignette|nucleo_sd]]
<p style="clear: both;" />'''Test d'initialisation'''


[[Fichier:Keyboard-layout.jpg|centré|vignette|575x575px|Keyboard layout]]
On commence par initialiser notre carte SD grâce à fichier.c et en premier lieu cela ne fonctionnait pas on avait status=1 et erreur=8 (et en conséquent une taille nulle puisqu'il n'arrive pas à initialiser la carte). La carte n'était pas de bonne qualité et ne communiquait pas en SPI mais sans doute avec un autre protocole. Mais par la suite avec la carte SD donnée par M. Redon, on a réussi à la phase d'initialisation.
<p style="clear: both;" />


L’utilisation de cet outil présente plusieurs intérêts : elle permet d’'''imaginer et définir la disposition du clavier''', de '''le découper en lignes et colonnes''' afin de concevoir la '''matrice de touches''', et enfin d’'''identifier les bonnes empreintes''' à utiliser sur le futur PCB grâce au '''sommaire illustré ci-dessous'''.
On obtient alors :


[[Fichier:Summary keyboard layout.png|centré|vignette|399x399px|Summary keyboard layout]]
* son type, ici 2 (l'autre type étant 1 pour les micro cartes sd moins performantes)
* ainsi que sa taille : 3 911 860 secteurs. Un secteur étant de 512 octets, on retrouve bien la taille écrite sur notre carte à savoir 2Gb.


[[Fichier:Sd init.png|center|600px|alt=Sd init|vignette|Sd init]]
<p style="clear: both;" />
<p style="clear: both;" />


Ce sommaire indique la '''taille indicative de chaque type de touche''' ainsi que '''le nombre de touches associées''' à chacune d’elles. Le "coloriage" est utile pour voir visuellement quelle touche correspond à quelle taille.
Suite à cela, on teste l'écriture, qui s'avère opérationelle (status = 1) puis l'écriture, elle aussi fonctionnelle puisque l'on affiche bien les 3 "blocs" voulus.


On peut également sauvegarder notre configuration en exportant sous format "json". Via ce format on peut utiliser une extension de kicad qui se prénomme "'''Keyboard footprints placer'''" et qui permet de placer automatiquement les boutons si on les intancie dans le bon ordre (exemple : bouton 1 => SW1 , etc...). L'outil est un peu capricieux mais fait gagner un temps précieux sur le routage.
[[Fichier:Sd read test.png|600px|alt=Sd read test|vignette|Sd read test]]
[[Fichier:Sd write test.png|600px|alt=Sd write test|vignette|Sd write test|gauche]]
<p style="clear: both;" />'''Commandes pour le système de fichier'''


La prochaine étape est de coder les commandes nécéssaires telles que append, read, remove, rename, copy.
# format : efface l'entièreté du système de fichier.
# list : liste les noms des fichiers contenus dans le système de fichier, équivalent du "ls" sous linux.
# append : créé un fichier si non existant et ajoute du texte si le fichier existe déjà. Commande de la forme append fichier/données. Combine le "touch" (create dans l'énoncé) et l'écriture de données.
# read : permet de lire le contenu d'un fichier, équivalent du "cat".
# remove : supprime le fichier en paramètre, équivalent du "rm".
# rename : renommer un fichier, équivalent du "mv" en moins puissant.
# copy : copie un fichier et ses données dans un second fichier, équivalent du "cp".
[[Fichier:Demo fileSystem.webm|center|500px|vignette|demo_fileSystem]]
<p style="clear: both;" />
<p style="clear: both;" />


==== Schématique ====
===== SPI =====
'''<u>Notre carte fille comporte plusieurs éléments :</u>'''
Nous allons programmer le SPI sur notre microcontrôleur et tester ce code pour la carte Sparkfun Serial 7-Segments Display. Sur ce git : <nowiki>https://github.com/sparkfun/Serial7SegmentDisplay/wiki/Serial-7-Segment-Display-Datasheet</nowiki> se trouve la datasheet de cette carte que nous allons utiliser pour communiquer en SPI. Dans l'idée, nous allons programmer une bibliothèque suffisament complète afin de pouvoir simplement afficher "9" via une commande dans ce style dans notre main : "spi_sent('G');"
# Le microcontrôleur ATMega32U4 avec un cristal de 16 MHz, des capacités de découplage et une ferrite (Cf AVR042) ;
 
# L'USB pour la programmation et l'alimentation pendant la phase programmation du projet ;
On peut tout d'abord centraliser la logique des initialisations des pins afin d'alléger le main (Cf fichier hardware_setup.c et .h) .
# Le connecteur ISP ;
[[Fichier:Carte7SegmentSparkFunPinout.png|centré|vignette|446x446px|7 segments SparkFun Pinout]]
# Les boutons RST et HWB ;
# Le connecteur SPI pour la communication avec la carte mère ;
# La led pour l'alimentation de la carte ;
# La led pour l'état du clavier (rôle ?) ;
# La matrice de touches évidemment ;
# Des mounting holes.


Remarque : Pas de leds RGB, pas assez de pins et nous ne voulions pas nous éparpiller sur trop d'idées (sujet evoqué avec Monsieur Boé).
==== Notre carte mère ====


[[Fichier:Clavier schematique.pdf|center|700px|alt=Clavier schematique|vignette|Clavier schematique]]
Afin de vérifier que notre PCB reçu fonctionne, on teste notre carte en faisant clignoter une led.<syntaxhighlight lang="c">
#include "../00-lib/stm32f410rx.h"


==== Vue 3D ====
#define LED_PIN 9
[[Fichier:Keyboard 3D up v2.png|gauche|vignette|652x652px|Keyboard 3D up]]
[[Fichier:Keyboard 3D back v2.png|vignette|649x649px|Keyboard 3D back]]
<p style="clear: both;" />


==== Brasure ====
int main(void) {
Nous avons soudé le stricte minimum sur notre carte pour le faire fonctioner avant tout puisque que nous souhaitons trouvé les éventuelles anomalies de soudure ou de conception avant chaque grosse étape. Sur la PCB rouge et la 1ère pcb verte tout est ok.
  // Activer horloge GPIOC
  RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOCEN_Pos);


[[Fichier:Clavier brasé.jpg|500px|alt=clavier brasé|vignette|clavier brasé|centré]]La seconde PCB verte en revanche n'est pas reconnu. On teste les différentes connexions au multimètre. Le 5V est bien là. On teste alors le quartz à l'oscilloscope. On se rend compte que l'on obtient que du bruit par rapport au pcb rouge. Cependant après avoir changé le quartz, le problème est toujours présent. On finit alors par se rendre compte que l'on a inversé une capacité avec une resistance, le problème est donc résolu rapidement.<p style="clear: both;" />
  GPIOC->MODER &= ~(0x3 << (LED_PIN * 2)); // Clear
  GPIOC->MODER |= 0x1 << (LED_PIN * 2);    // Output
  GPIOC->OTYPER &= ~(1 << LED_PIN);       // Push-pull
  GPIOC->PUPDR &= ~(0x3 << (LED_PIN * 2)); // No pull


[[Fichier:Test oscillo.jpg|left|400px|alt=test_oscillo|vignette|test_oscillo]]
  while (1) {
[[Fichier:Oscillo vert.jpg|right|300px|alt=oscillo_vert|vignette|oscillo_vert]]
    GPIOC->ODR ^= (1 << LED_PIN);
[[Fichier:Oscillo rouge.jpg|right|300px|alt=oscillo_rouge|vignette|oscillo_rouge]]
    for (volatile uint32_t i = 0; i < 1000000; i++)
      ;
  }
}


<p style="clear: both;" />
</syntaxhighlight>Maintenant que la carte à été testée nous avons fait plusieurs codes intermediaires pour tester chaque primitive séparément. Ici le détail du code final seulement. 


=== Software ===
===== Carte =====
Dans ce dossier nous retrouverons toutes les librairies liées au contrôle des cartes filles correspondantes.


==== Test Led ====
====== Carte ecran ======
Afin de vérifier que notre clavier fonctionne, on fait un test afin de faire clignoter nos deux leds : led d'alimentation et led pour Cap Lock (qui nous servira par la suite pour savoir si notre carte est en mode majuscule ou non).
L'idée est d'avoir une librairie permettant de contrôler une carte écran et d'afficher un compteur sur celui-ci. Notre carte écran est ici l'afficheur 7 segments de Sparkfun.  


Remarque : la fonction setupPin est la même que celle présentée dans la section ordonnanceur de la carte shield.<syntaxhighlight lang="c">
carteEcran.c :<syntaxhighlight lang="c">
#define F_CPU 16000000UL
#include "carteEcran.h"
#include <stdint.h>


#define LEDs_PORT PORTE
// PC11, CS5
#define LEDs_DDR DDRE
#define CS5_GPIO GPIOC
#define LEDs_PIN PINE
#define CS5_PIN 11
#define LED_CapsLock PE6


void setupHardware() {
// PB9, RST5
  setupClock();
#define RST5_GPIO GPIOB
  // Leds
#define RST5_PIN 9
  setupPin(&LEDs_PORT, &LEDs_DDR, LED_CapsLock, OUTPUT);


  // Bouton
// PC13, INT5
  //setupPin(BTNs_PORT, BTNs_DDR, BTN_Right, INPUT_PULL_UP);
#define INT5_GPIO GPIOC
#define INT5_PIN 13


  // Permet de liberer le portF pour utiliser les boutons !
uint8_t isChange = 0b1111;
  MCUCR |= (1 << JTD); // 1ère écriture
  MCUCR |= (1 << JTD); // Désactiver JTAG (2ème écriture obligatoire !)
}


int main() {
int unite = 0;
  setupHardware();
int decimal = 0;
  while (1) {
int centaine = 0;
      LEDs_PORT |= (1 << LED_CapsLock); // toggle LED
int mil = 0;
      _delay_ms(500);
      LEDs_PORT &= ~(1 << LED_CapsLock); // toggle LED
      _delay_ms(500);
  }
    return 0;
}


</syntaxhighlight>
void _ecran_init() {
  setupPin(CS5_GPIO, CS5_PIN, OUTPUT);
  setupPin(RST5_GPIO, RST5_PIN, OUTPUT);
  // setupPin(INT5_GPIO, INT5_PIN, OUTPUT);


==== Détection de notre matrice de boutons ====
  // Ces pins s'active à l'etat bas
  onPin(CS5_GPIO, CS5_PIN); // CS OFF


On créer un fichier qui pourra être facilement importer dans nos différents projets afin d'avoir une détection de touche facilement importer dans la suite : la LUFA.
  offPin(RST5_GPIO, RST5_PIN); // RST ON
  onPin(RST5_GPIO, RST5_PIN);  // RST OFF


Voici alors clavier.c :<syntaxhighlight lang="c">
  // Non utilisé ici
#include "clavier.h"
  //  onPin(INT5_GPIO, INT5_PIN); // S'active à l'etat bas ?
}


#include "../lib/HARDWARE/hardware.h"
void ecran_spi_write(uint8_t data) {
#include <avr/io.h>
  spi_write(data, CS5_GPIO, CS5_PIN);
}


// --------- Colonnes ---------
void ecran_brightness(uint8_t intensite) {
volatile uint8_t *col_ports[TOTAL_COL] = {
   ecran_spi_write(0x7A);      // Commande "Brightness"
    [0 ... 5] = &PORTF,   // COL0 à COL5
   ecran_spi_write(intensite); // Sécurité passive 2^8-1 = 255 qui est le maximum
    [6 ... 7] = &PORTC,   // COL6 à COL7
}
    [8 ... 10] = &PORTB,  // COL8 à COL10
 
    [11 ... 13] = &PORTD, // COL11 à COL13
void ecran_clear() {
};
  ecran_spi_write(0x76); // Commande "Clear"
}


volatile uint8_t *col_ddr[TOTAL_COL] = {
void ecran_select_digit(uint8_t digit) {
    [0 ... 5] = &DDRF,   // COL0 à COL5
   // Sécurité maximum digit
    [6 ... 7] = &DDRC,   // COL6 à COL7
   if (digit >= 3)
    [8 ... 10] = &DDRB,  // COL8 à COL10
     digit = 3;
     [11 ... 13] = &DDRD, // COL11 à COL13
};


volatile uint8_t *col_pins_reg[TOTAL_COL] = {
   ecran_spi_write(0x79);  // Cursor command
    [0 ... 5] = &PINF,   // COL0 à COL5
   ecran_spi_write(digit); // 0 à 3
    [6 ... 7] = &PINC,   // COL6 à COL7
}
    [8 ... 10] = &PINB,  // COL8 à COL10
    [11 ... 13] = &PIND, // COL11 à COL13
};


uint8_t col_pins[TOTAL_COL] = {0, 1, 4, 5, 6, 7, 7, 6, 6, 5, 4, 7, 6, 4};
void ecran_compteur() {


// --------- Lignes ---------
  ecran_select_digit(3);
volatile uint8_t *row_ports[TOTAL_ROW] = {
  ecran_spi_write(unite); // Data
    [0 ... 4] = &PORTD,
};


volatile uint8_t *row_ddr[TOTAL_ROW] = {
  if (isChange & 0b0010) {
     [0 ... 4] = &DDRD,
     isChange &= ~0b0010;
};
    ecran_select_digit(2);
    ecran_spi_write(decimal); // Data
  }


uint8_t row_pins[TOTAL_ROW] = {5, 3, 2, 1, 0};
  if (isChange & 0b0100) {
    isChange &= ~0b0100;
    ecran_select_digit(1);
    ecran_spi_write(centaine); // Data
  }


uint8_t key_state[TOTAL_COL][TOTAL_ROW] = {0};
  if (isChange & 0b1000) {
    isChange &= ~0b1000;
    ecran_select_digit(0);
    ecran_spi_write(mil); // Data
  }


void init_matrix_button(void) {
  if (unite < 9) {
   // Configuration colonnes en entrée avec pull-up
    unite++;
  for (uint8_t c = 0; c < TOTAL_COL; c++) {
   } else {
    setupPin(col_ports[c], col_ddr[c], col_pins[c], INPUT_PULL_UP);
    isChange |= 0b0010;
  }
    unite = 0;
    if (decimal < 9)
      decimal++;
    else {
      decimal = 0;
      isChange |= 0b0100;


  // Configuration ligne en sortie
      if (centaine < 9) {
  for (uint8_t r = 0; r < TOTAL_ROW; r++) {
        centaine++;
    setupPin(row_ports[r], row_ddr[r], row_pins[r], OUTPUT);
      } else {
    onPin(row_ports[r], row_pins[r]); // mettre toutes les lignes à 1 pour les desactiver
        centaine = 0;
  }
        isChange |= 0b1000;
}


void scan() {
        if (mil < 9) {
  for (uint8_t r = 0; r < TOTAL_ROW; r++) {
          mil++;
    offPin(row_ports[r], row_pins[r]); // activer ligne (LOW)
        } else {
 
          mil = 0;
    for (uint8_t c = 0; c < TOTAL_COL; c++)
        }
       key_state[c][r] = !(*col_pins_reg[c] & (1 << col_pins[c]));
       }
 
     }
     onPin(row_ports[r], row_pins[r]); // désactiver ligne (HIGH)
   }
   }
}
}
</syntaxhighlight>Et son .h : <syntaxhighlight lang="c">
#pragma once


</syntaxhighlight>Et clavier.h : <syntaxhighlight lang="c">
#include "../../SPI/spi.h"
#ifndef CLAVIER_H
#include "../../GPIO/gpio.h"
#define CLAVIER_H
 
// Fonction inter librairie
void _ecran_init();


#include "keyswitch.h"
// Fonctions pour l'utilisateur
#include <stdint.h>
void ecran_spi_write(uint8_t data);
#include "clavier_conversion.h"
void ecran_brightness(uint8_t intensite);
void ecran_select_digit(uint8_t digit);


extern uint8_t key_state[TOTAL_COL][TOTAL_ROW];
// Fonction exemple pour utilisateur
void ecran_compteur();
</syntaxhighlight>Les commandes de l'écran ont été trouvé sur leur site : https://learn.sparkfun.com/tutorials/using-the-serial-7-segment-display/all


void init_matrix_button(void);
Il faut noter que la vitesse du SPI peut influencer la précision du SPI et donc faire échouer certains messages si la vitesse dépasse 9600 bauds rate.
void scan(void);


#endif
====== Carte clavier ======
</syntaxhighlight>Et ensuite nous avons pleins de define afin de lire chaque bouton individuellement au lieu d'appeler un tableau (pas intuitif pour l'utilisateur) dans un fichier keyswitch.h :<syntaxhighlight lang="c">
Cette partie est assez complexe et sera implémentée plus tard.
#ifndef KEYSWITCH_H
#define KEYSWITCH_H


#include <stdint.h>
===== Commande OS =====
Cette librairie permet d'implémenter les commandes liés à notre OS tel que "version", "echo, et "devices".


#define TOTAL_KEYSWITCH 62
cmd_os.c: <syntaxhighlight lang="c">
#define TOTAL_COL 14
#include "cmd_os.h"
#define TOTAL_ROW 5


#include "../DEVICES/devices.h"
#include "../Substitute/printf.h"
#include "../TASK/task.h"


// COL0 PF0 | COL1 PF1 | COL2 PF4 | COL3  PF5 | COL4  PF6 | COL5  PF7 | COL6  PC7 |
#include <string.h>
// COL7 PC6 | COL8 PB6 | COL9 PB5 | COL10 PB4 | COL11 PD7 | COL12 PD6 | COL13 PD4
typedef enum{
    COL0 = 0,
    COL1 = 1,
    COL2 = 2,
    COL3 = 3,
    COL4 = 4,
    COL5 = 5,
    COL6 = 6,
    COL7 = 7,
    COL8 = 8,
    COL9 = 9,
    COL10 = 10,
    COL11 = 11,
    COL12 = 12,
    COL13 = 13,
} COLs;


// ROW0 PD5 | ROW1 PD3 | ROW2 PD2 | ROW3 PD1 | ROW4 PD0
#define MAX_ARG_ECHO 20
typedef enum{
#define CMD_SIZE 128
    ROW0 = 0,
    ROW1 = 1,
    ROW2 = 2,
    ROW3 = 3,
    ROW4 = 4,
} ROWs;


extern uint8_t key_state[TOTAL_COL][TOTAL_ROW];
#define CHAR_CODE_ESC 0x1B
#define CHAR_CODE_ENTER '\r'
#define CHAR_CODE_BACKSPACE1 '\b'
#define CHAR_CODE_BACKSPACE2 0x7F


#define btn1  key_state[COL0][ROW0]
typedef enum {
#define btn2  key_state[COL1][ROW0]
  KEY_NONE,
#define btn3  key_state[COL2][ROW0]
  KEY_CHAR,
#define btn4  key_state[COL3][ROW0]
  KEY_ENTER,
#define btn5  key_state[COL4][ROW0]
  KEY_BACKSPACE,
#define btn6  key_state[COL5][ROW0]
  KEY_ARROW_UP,
#define btn7  key_state[COL6][ROW0]
  KEY_ARROW_DOWN,
#define btn8  key_state[COL7][ROW0]
  KEY_ARROW_LEFT,
#define btn9  key_state[COL8][ROW0]
  KEY_ARROW_RIGHT
#define btn10 key_state[COL9][ROW0]
} KeyType;
#define btn11 key_state[COL10][ROW0]
 
#define btn12 key_state[COL11][ROW0]
typedef enum {
#define btn13 key_state[COL12][ROW0]
  NORMAL_CHAR = 0,
#define btn14 key_state[COL13][ROW0]
  ESC_CHAR = 1,
  ARROW_CHAR = 2,
} _stateKey;
 
typedef struct {
  KeyType type;
  char ch;
} KeyEvent;
 
char current_cmd[CMD_SIZE + 1] = {'\0'};
static uint8_t cmd_len = 0;
// static uint8_t cursor_pos = 0;
 
_stateKey esc_state = NORMAL_CHAR;
KeyEvent ev = {KEY_NONE, 0};


#define btn15 key_state[COL0][ROW1]
KeyEvent _decode_key(char c) {
#define btn16 key_state[COL1][ROW1]
  switch (esc_state) {
#define btn17 key_state[COL2][ROW1]
  case NORMAL_CHAR:
#define btn18 key_state[COL3][ROW1]
    ev.type = KEY_NONE;
#define btn19 key_state[COL4][ROW1]
    ev.ch = 0;
#define btn20 key_state[COL5][ROW1]
    if (c == CHAR_CODE_ESC) { // ESC
#define btn21 key_state[COL6][ROW1]
      esc_state = ESC_CHAR;
#define btn22 key_state[COL7][ROW1]
      return ev; // attente suite
#define btn23 key_state[COL8][ROW1]
    }
#define btn24 key_state[COL9][ROW1]
 
#define btn25 key_state[COL10][ROW1]
    if (c == CHAR_CODE_ENTER) {
#define btn26 key_state[COL11][ROW1]
      ev.type = KEY_ENTER;
#define btn27 key_state[COL12][ROW1]
    } else if (c == CHAR_CODE_BACKSPACE1 || c == CHAR_CODE_BACKSPACE2) {
#define btn28 key_state[COL13][ROW1]
      ev.type = KEY_BACKSPACE;
    } else {
      ev.type = KEY_CHAR;
      ev.ch = c;
    }
    return ev;


#define btn29 key_state[COL0][ROW2]
  case ESC_CHAR:
#define btn30 key_state[COL1][ROW2]
    if (c == '[' || c == 'O') {
#define btn31 key_state[COL2][ROW2]
      esc_state = ARROW_CHAR;
#define btn32 key_state[COL3][ROW2]
    } else {
#define btn33 key_state[COL4][ROW2]
      esc_state = NORMAL_CHAR; // ESC seul = abandon
#define btn34 key_state[COL5][ROW2]
    }
#define btn35 key_state[COL6][ROW2]
    return ev;
#define btn36 key_state[COL7][ROW2]
#define btn37 key_state[COL8][ROW2]
#define btn38 key_state[COL9][ROW2]
#define btn39 key_state[COL10][ROW2]
#define btn40 key_state[COL11][ROW2]
#define btn41 key_state[COL12][ROW2]


#define btn42 key_state[COL0][ROW3]
  case ARROW_CHAR:
#define btn43 key_state[COL1][ROW3]
    switch (c) {
#define btn44 key_state[COL2][ROW3]
    case 'A':
#define btn45 key_state[COL3][ROW3]
      ev.type = KEY_ARROW_UP;
#define btn46 key_state[COL4][ROW3]
      break;
#define btn47 key_state[COL5][ROW3]
    case 'B':
#define btn48 key_state[COL6][ROW3]
      ev.type = KEY_ARROW_DOWN;
#define btn49 key_state[COL7][ROW3]
      break;
#define btn50 key_state[COL8][ROW3]
    case 'C':
#define btn51 key_state[COL9][ROW3]
      ev.type = KEY_ARROW_RIGHT;
#define btn52 key_state[COL10][ROW3]
      break;
#define btn53 key_state[COL11][ROW3]
    case 'D':
#define btn54 key_state[COL13][ROW3]
      ev.type = KEY_ARROW_LEFT;
      break;
    default:
      // ignore les autres codes
      break;
    }
    esc_state = NORMAL_CHAR;
    return ev;
  }


#define btn55 key_state[COL0][ROW4]
  ev.type = KEY_NONE;
#define btn56 key_state[COL1][ROW4]
  ev.ch = 0;
#define btn57 key_state[COL2][ROW4]
  esc_state = NORMAL_CHAR;
#define btn58 key_state[COL6][ROW4]
  return ev;
#define btn59 key_state[COL10][ROW4]
}
#define btn60 key_state[COL11][ROW4]
#define btn61 key_state[COL12][ROW4]
#define btn62 key_state[COL13][ROW4]


#endif
void _cmdOS_inconnu() {
</syntaxhighlight>La bivliothèque à été penser pour être facilement réadaptable et lisible pour un utilisateur souhaitant re coder un clavier mais également pour quelqu'un ne souhaitant pas refaire l'hardware et juste plug and play celui déjà fait.
  PRINT_STRING("\r\nCommande inconnue : ");
  PRINT_STRING(current_cmd);
  PRINT_STRING("\r\n");
}


==== LUFA ====
void _cmdOS_version() {
Afin que nos touches soient reconnus comme des lettres de l'alphabet que l'on peut voir sur notre écran, on utilise la LUFA. Rien de bien compliqué, on retrouve le projet LUFA sur ce lien github : https://github.com/abcminiuser/lufa
  PRINT_STRING("\r\nOS : version 0.0.2\n\r");
}


On télécharge le projet et on vient extraire la librairie LUFA et l'exemple  Keyboard présent dans le dossier Démo. On modifie le Makefile présent ainsi : <syntaxhighlight lang="makefile">
void _cmdOS_echo(char *input) {
MES_LIBS = lib/HARDWARE/hardware.c lib/CLAVIER/clavier.c
  char *argv[MAX_ARG_ECHO];
  int argc = 0;


MCU          = atmega32u4
  // Découpe la ligne en mots
ARCH        = AVR8
  char *token = strtok(input, " ");
BOARD        = NONE
  while (token != NULL && argc < MAX_ARG_ECHO) {
F_CPU        = 16000000
    argv[argc++] = token;
F_USB        = $(F_CPU)
    token = strtok(NULL, " ");
OPTIMIZATION = s
  }
TARGET       = Keyboard
 
SRC          = $(TARGET).c Descriptors.c $(MES_LIBS) $(LUFA_SRC_USB) $(LUFA_SRC_USBCLASS)
  if (argc > 0) {
LUFA_PATH    = ../LUFA
    PRINT_STRING("\r");
CC_FLAGS    = -DUSE_LUFA_CONFIG_HEADER -IConfig/
    for (int i = 1; i < argc; i++) {
LD_FLAGS    =
       PRINT_STRING(argv[i]);
      if (i + 1 < argc)
        PRINT_STRING(" ");
    }
    PRINT_STRING("\r\n\n");
  }
}
 
void _cmdOS_devices(void) {
  scan_devices();
  print_devices();
}


# Default target
void _execute_command(char *cmd) {
all:
  if (strcmp(cmd, "version") == 0)
    _cmdOS_version();
  else if (strcmp(cmd, "devices") == 0)
    _cmdOS_devices();
  else if (strncmp(cmd, "echo", strlen("echo")) == 0)
    _cmdOS_echo(cmd);
  else if (cmd_len > 0)
    _cmdOS_inconnu();
}


# Include LUFA-specific DMBS extension modules
void _init_os() {
DMBS_LUFA_PATH ?= $(LUFA_PATH)/Build/LUFA
  PRINT_STRING("Initialisation du Pico ordinateur ");
include $(DMBS_LUFA_PATH)/lufa-sources.mk
include $(DMBS_LUFA_PATH)/lufa-gcc.mk


# Include common DMBS build system modules
  for (int i = 0; i < 6; i++) {
DMBS_PATH      ?= $(LUFA_PATH)/Build/DMBS/DMBS
    task_delay(100);
include $(DMBS_PATH)/core.mk
    PRINT_STRING(".");
include $(DMBS_PATH)/cppcheck.mk
  }
include $(DMBS_PATH)/doxygen.mk
 
include $(DMBS_PATH)/dfu.mk
  PRINT_STRING("\n\rPicoOrdi>");
include $(DMBS_PATH)/gcc.mk
}
include $(DMBS_PATH)/hid.mk
include $(DMBS_PATH)/avrdude.mk
include $(DMBS_PATH)/atprogram.mk


PROGRAMMER = avrdude
void cmd_os(void) {
AVRDUDE_PORT = /dev/ttyACM0
  _init_os();
AVRDUDE_BAUD = 115200
AVRDUDE_PROGRAMMER = avr109


upload: $(TARGET).hex
  while (1) {
$(PROGRAMMER) -v -p $(MCU) -c $(AVRDUDE_PROGRAMMER) \
    while (WAITCHAR())
-P $(AVRDUDE_PORT) -b $(AVRDUDE_BAUD) -D \
      ;
-U flash:w:$(TARGET).hex:i
</syntaxhighlight>La ligne upload est necessaire seulement si vous avez un bootloader personnalisé, auquel cas faite un simple make dfu pour téléverser.


    char c = GETCHAR();
    KeyEvent key = _decode_key(c);


Ensuite on vient ajouter nos libraries aux fichiers Keyboard.c : <syntaxhighlight lang="c">
    switch (key.type) {
#include "./lib/CLAVIER/clavier.h"
    case KEY_CHAR:
#include "./lib/HARDWARE/hardware.h"
      if (cmd_len < CMD_SIZE) {
        current_cmd[cmd_len++] = key.ch;
        current_cmd[cmd_len] = '\0';
        PRINT_CHAR(key.ch);
      }
      break;


// ... je passe les détails basique du fichier
    case KEY_BACKSPACE:
      if (cmd_len > 0) {
        cmd_len--;
        current_cmd[cmd_len] = '\0';
        PRINT_STRING("\b \b");
      }
      break;


void SetupHardware() {
    case KEY_ENTER:
  /* Disable watchdog if enabled by bootloader/fuses */
      PRINT_STRING("\r\n");
  MCUSR &= ~(1 << WDRF);
      _execute_command(current_cmd);
  wdt_disable();
      cmd_len = 0;
      current_cmd[0] = '\0';
      PRINT_STRING("PicoOrdi>");
      break;
 
    case KEY_ARROW_UP:
      PRINT_STRING("\r\nUP - Gestion historique a implementer\r\nPicoOrdi>");
      break;
 
    case KEY_ARROW_DOWN:
      PRINT_STRING("\r\nDOWN - Gestion historique a implementer\r\nPicoOrdi>");
      break;
 
    case KEY_ARROW_LEFT:
      PRINT_STRING("\x1B[D");
      cmd_len--;
      break;


  /* Disable clock division */
    case KEY_ARROW_RIGHT:
  clock_prescale_set(clock_div_1);
      PRINT_STRING("\x1B[C");
      cmd_len++;
      break;


  /* Hardware Initialization */
    default:
  setupHardware();
      break;
  init_matrix_button();
    }
    
   }
  // Initialisation USB obligatoire
  USB_Init();
}
}
</syntaxhighlight>cmd_os.h :<syntaxhighlight lang="c">
#ifndef CMD_OS_H
#define CMD_OS_H
#include "../../../00-lib/stm32f410rx.h"


// ... je passe les détails basique du fichier


bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t *const HIDInterfaceInfo,
/*
                                        uint8_t *const ReportID,
    Permet de lancer les commandes suivantes :
                                        const uint8_t ReportType,
   
                                        void *ReportData,
    version => Affiche la version du système d'exploitation
                                        uint16_t *const ReportSize) {
    echo => Affiche les arguments de la commande
    devices => Scan toutes les cartes et retourne celle connecté
*/


  USB_KeyboardReport_Data_t *KeyboardReport = (USB_KeyboardReport_Data_t *)ReportData;
void cmd_os(void); // Affiche la liste des cartes filles connectées
uint8_t UsedKeyCodes = 0;


  scan();
#endif
 
 
    // ROW0
</syntaxhighlight>
    if (btn1)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_ESCAPE);
 
    if (btn2 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_1_AND_EXCLAMATION);
Pour le moment la gestion du terminal permet de gérer la détection des flèches grâce à une implémentation de la détection de touches avec plusieurs états.
    if (btn3 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_2_AND_AT);
 
    if (btn4 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_3_AND_HASHMARK);
===== Devices =====
    if (btn5 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_4_AND_DOLLAR);
Cette librairie permet de gérer la détection des cartes filles (généralement après un appel de la fonction "devices").
    if (btn6 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_5_AND_PERCENTAGE);
 
    if (btn7 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_6_AND_CARET);
device.c :<syntaxhighlight lang="c">
    if (btn8 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_7_AND_AMPERSAND);
#include "devices.h"
    if (btn9 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_8_AND_ASTERISK);
#include "../SPI/spi.h"
    if (btn10 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_9_AND_OPENING_PARENTHESIS);
#include "../Substitute/printf.h"
    if (btn11 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_0_AND_CLOSING_PARENTHESIS);
#include <stdio.h>
    if (btn12 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_MINUS_AND_UNDERSCORE); // ) ° ]
    if (btn13 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_EQUAL_AND_PLUS); // = + }
    if (btn14) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_BACKSPACE);


    // ROW1
static const GPIO_TypeDef *cs_gpios[MAX_DEVICES] = {
    if (btn15) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_TAB);
     [CS1] = GPIOC, // PC0
     if (btn16) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_A);
     [CS2] = GPIOA, // PA7
     if (btn17) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Z);
     [CS3] = GPIOC, // PC9
     if (btn18) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_E);
     [CS4] = GPIOA, // PA2
     if (btn19) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_R);
     [CS5] = GPIOC, // PC11
     if (btn20) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_T);
     [CS6] = GPIOA, // PA4
     if (btn21) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Y);
};
    if (btn22) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_U);
 
     if (btn23) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_I);
static const uint8_t cs_pins[MAX_DEVICES] = {
     if (btn24) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_O);
     [CS1] = 0,  // PC0
     if (btn25) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_P);
     [CS2] = 7,  // PA7
     if (btn26) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_OPENING_BRACKET_AND_OPENING_BRACE); //
     [CS3] = 9,  // PC9
     if (btn27) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_CLOSING_BRACKET_AND_CLOSING_BRACE); // $£¤
     [CS4] = 2,  // PA2
     if (btn28) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_ENTER);
     [CS5] = 11, // PC11
     [CS6] = 4,  // PA4
};


    // ROW2
Device devices[MAX_DEVICES];
    if (btn29) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_CAPS_LOCK);
    if (btn30) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Q);
    if (btn31) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_S);
    if (btn32) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_D);
    if (btn33) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F);
    if (btn34) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_G);
    if (btn35) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_H);
    if (btn36) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_J);
    if (btn37) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_K);
    if (btn38) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_L);
    if (btn39) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_M);
    if (btn40) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_APOSTROPHE_AND_QUOTE); // % ù
    if (btn41) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_NON_US_HASHMARK_AND_TILDE); // µ *


    // ROW3
static const char *device_names[] = {
    if (btn42) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_SHIFT);
    "NOT CONNECTED",
    if (btn43) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_NON_US_BACKSLASH_AND_PIPE);
    "UNKNOWN",
     if (btn44) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_W);
    "KEYBOARD",
     if (btn45) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_X);
    "SCREEN",
     if (btn46) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_C);
    "NETWORK",
     if (btn47) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_V);
    "SOUND",
    if (btn48) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_B);
    "SD"};
     if (btn49) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_N);
 
     if (btn50) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN); // ,?
static const char *cs_names[] = {
     // if (btn51) KeyboardReport->KeyCode[UsedKeyCodes++] = ; // ;.
    "PC0",
     if (btn52) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_DOT_AND_GREATER_THAN_SIGN); // :/
    "PA7",
     if (btn53 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_SLASH_AND_QUESTION_MARK);  // !§
     "PC9",
     if (btn54) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_RIGHT_SHIFT);
     "PA2",
     "PC11",
     "PA4"};
 
static const char *names[] = {
     "Clavier",
     "FPGA",
     "Son",
     "Reseau",
     "Ecran",
     "SD"};


   // ROW4
void _devices_init() {
   if (btn55) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_LEFTCTRL);
   // Init tableau
  if (btn56) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_LEFTGUI);
   for (int i = 0; i < MAX_DEVICES; i++) {
  if (btn57) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_ALT);
    devices[i].GPIO_CS = (GPIO_TypeDef *)cs_gpios[i];
  if (btn58) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_SPACE);
    devices[i].PIN_CS = cs_pins[i];
  if (btn59 && !btn62) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_RIGHTALT);
    devices[i].type = UNKNOWN;
  // if (btn60 && !btn62) KeyboardReport->Keyboard |= QWERTY_to_AZERTY(); // Trouver une fonction a celui ci
   }
   if (btn61 && !btn62) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_RIGHTCTRL);


   // Fonction spéciale du clavier côté HARDWARE
   // SD toujours ici
   if(btn62){
  devices[5].type = SD; // Peut etre tester si connecte ou non
    // Fonction F1 à F12
 
    if (btn2) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F1);
  // Scan du démarrage
    if (btn3)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F2);
   scan_devices();
     if (btn4)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F3);
}
     if (btn5)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F4);
 
     if (btn6)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F5);
void scan_devices() {
     if (btn7)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F6);
// --------------------------------------------------------------------------------
     if (btn8)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F7);
// A ajouter a la fin
     if (btn9)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F8);
// --------------------------------------------------------------------------------
     if (btn10) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F9);
}
     if (btn11) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F10);
 
     if (btn12) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F11);
void print_devices() {
     if (btn13) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F12);
  PRINT_STRING("\r\nDevice list:\r\n");
  for (int device = 0; device < MAX_DEVICES; device++) {
     char buffer[15];
 
    sprintf(buffer, "%d", device);
     PRINT_STRING("N°");
     PRINT_STRING(buffer);
     PRINT_STRING(": ");
     PRINT_STRING(device_names[devices->type]);
 
     sprintf(buffer, "%d", device + 1);
     PRINT_STRING("\t\tCS");
     PRINT_STRING(buffer);
     PRINT_STRING(": ");
     PRINT_STRING(cs_names[device]);


    // Déplacement
     if (device < 10)
    if (btn60) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_DOWN_ARROW);
      PRINT_STRING(" ");
    if (btn59) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_ARROW);
     if (btn61) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_RIGHT_ARROW);
    if (btn53) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_UP_ARROW);


    PRINT_STRING("\t\tName port: ");
    PRINT_STRING(names[device]);
    PRINT_STRING("\r\n");
   }
   }
}


  *ReportSize = sizeof(USB_KeyboardReport_Data_t);
Device get_state_device(DeviceCS CSx) {
   return false;
   return devices[CSx];
}
}


void CALLBACK_HID_Device_ProcessHIDReport(USB_ClassInfo_HID_Device_t *const HIDInterfaceInfo,
</syntaxhighlight>
                                          const uint8_t ReportID,
 
                                          const uint8_t ReportType,
device.h :<syntaxhighlight lang="c">
                                          const void *ReportData,
#ifndef DEVICES_H
                                          const uint16_t ReportSize) {
#define DEVICES_H
 
 
  uint8_t *LEDReport = (uint8_t *)ReportData;
#include "../../../00-lib/stm32f410rx.h"
 
#define MAX_DEVICES 6
 
typedef enum {
    NOT_CONNECTED = 0,
    UNKNOWN,
    KEYBOARD,
    SCREEN,
    NETWORK,
    SOUND,
    SD,
} DeviceType;


  if (*LEDReport & HID_KEYBOARD_LED_CAPSLOCK)
typedef struct {
     onPin(LEDs_PORT,LED_CapsLock);
    GPIO_TypeDef *GPIO_CS;        // GPIO Device Select
  else
     uint8_t PIN_CS;               // Pin Device Select
    offPin(LEDs_PORT,LED_CapsLock);
    DeviceType type;              // Type de device
} Device;


}
typedef enum {
    CS1 = 0, // PC0
    CS2 = 1, // PA7
    CS3 = 2, // PC9
    CS4 = 3, // PA2
    CS5 = 4, // PC11
    CS6 = 5, // PA4
} DeviceCS;


void _devices_init(void);
void scan_devices(void);
Device get_state_device(DeviceCS CSx);
void print_devices(void);


#endif
</syntaxhighlight>
</syntaxhighlight>


===== GPIO =====
Cette librairie permet d'initialiser les pins de notre carte via des fonctions intermediaires pour une meilleure lisibilité. Elle permet aussi de contrôler et lire les pins si besoin.


J'ai fais une petite macro (repris de l'année dernière, projet manette) pour convertir les caractères sur un clavier AZERTY dans le fichier clavier_conversion.h :<syntaxhighlight lang="c">
gpio.c :<syntaxhighlight lang="c">
#ifndef CLAVIER_CONVERSION_H
#include "./gpio.h"
#define CLAVIER_CONVERSION_H
 
#include "../CARTE/Ecran/carteEcran.h"
#include "../DEVICES/devices.h"
#include "../SPI/spi.h"
#include "../USART/usart.h"


#include <../../LUFA/Drivers/USB/USB.h>
#define OFFSET_BSRR_OFF 16


static inline uint8_t QWERTY_to_AZERTY(uint8_t qwerty_code) {
#define MODER_CLEAR 0x3
    switch (qwerty_code) {
#define MODER_NumberBitParPin 0x2
        // Lettres
        case HID_KEYBOARD_SC_Q: return HID_KEYBOARD_SC_A;
        case HID_KEYBOARD_SC_W: return HID_KEYBOARD_SC_Z;
        case HID_KEYBOARD_SC_A: return HID_KEYBOARD_SC_Q;
        case HID_KEYBOARD_SC_Z: return HID_KEYBOARD_SC_W;
        case HID_KEYBOARD_SC_M: return HID_KEYBOARD_SC_SEMICOLON_AND_COLON;
        case HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN: return HID_KEYBOARD_SC_M;       
        default: return qwerty_code;
    }
}


static inline uint8_t AZERTY_to_QWERTY(uint8_t azerty_code) {
#define PUPDR_CLEAR 0x3
    switch (azerty_code) {
#define PUPDR_NumberBitParPin 0x2
        // Lettres
        case HID_KEYBOARD_SC_A: return HID_KEYBOARD_SC_Q;
        case HID_KEYBOARD_SC_Z: return HID_KEYBOARD_SC_W;
        case HID_KEYBOARD_SC_Q: return HID_KEYBOARD_SC_A;
        case HID_KEYBOARD_SC_W: return HID_KEYBOARD_SC_Z;
        case HID_KEYBOARD_SC_SEMICOLON_AND_COLON: return HID_KEYBOARD_SC_M;
        case HID_KEYBOARD_SC_M: return HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN;
        default: return azerty_code;
    }
}


#endif
#define OTYPER_CLEAR 0x1
 
void setupPin(GPIO_TypeDef *GPIOx, uint8_t PINx, portModeRegister portMode) {
  // ACTIVATION DE L'HORLOGE GPIO SPECIFIQUE
  uint32_t RCC_AHB1ENR_GPIOxEN_Pos;
 
  if (GPIOx == GPIOA)
    RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOAEN_Pos;
  else if (GPIOx == GPIOB)
    RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOBEN_Pos;
  else if (GPIOx == GPIOC)
    RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOCEN_Pos;
  else
    return; // GPIO non existant sur ce microcontroleur
 
  if (!(RCC->AHB1ENR & (1 << RCC_AHB1ENR_GPIOxEN_Pos)))
    RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOxEN_Pos);


</syntaxhighlight>J'ai eu beaucoup de mal a trouver certains caractères mais le document présent ci dessous m'as grandement aidé :
  // CLEAR AVANT MODIFICATION
[[Fichier:HID Usage Tables.pdf|centré|vignette|HID Usage Tables]]
  // On clear après l'activation d'horloge !
A partir de la page 89 nous avons l'ensemble des codes et certains détails pour certains d'entre eux. Malgré tout une touche n'as pas réussi à être mapper (je n'ai pas trouvé le code équivalent pour mon clavier AZERTY), cette touche correspond au point virgule / point (;.).
  GPIOx->MODER &= ~(MODER_CLEAR << (PINx * MODER_NumberBitParPin));
  GPIOx->PUPDR &= ~(PUPDR_CLEAR << (PINx * PUPDR_NumberBitParPin));
  GPIOx->OTYPER &= ~(OTYPER_CLEAR << PINx); // Push-pull


Cependant le code reste fonctionnelle, j'y ai ajouté la possiblité de se deplacer via la touche FN qui correspond aux fonctionnalité spéciale, comme toutes les touches F1,F2,...F10,F11,F12 qui sont mappé aux touches 1,2,...,0,°,+.
  // TYPE DE PORT (Input, Output, Alternative, Analogique)
  portPullUpPullDownRegister typePull = NO_PULL;
  uint8_t optionPort = 0x00;
 
  switch (portMode) {
  case INPUT:
    optionPort = 0x00;
    typePull = PULL_UP;
    break;
 
  case OUTPUT:
    optionPort = 0x01;
    typePull = NO_PULL;
 
    if (1)
      GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
    else
      GPIOx->OTYPER |= (1 << PINx); // Open-drain
 
    break;
 
  case ALTERNATE_FUNCTION:
    optionPort = 0x02;
 
    if (1) {
      typePull = NO_PULL;
      GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
      GPIOx->OSPEEDR |= 11 << (PINx * 2);
      // Very high speed => Cf STM32F410 datasheet tableau p95/142
    }
 
    break;
  case ALTERNATE_FUNCTION_USART:
    optionPort = 0x02;
 
    typePull = NO_PULL;
    GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
    // GPIOx->OSPEEDR |= 11 << (PINx * 2); // Very high speed => Cf STM32F410 datasheet tableau p95/142
    if (GPIOx == GPIOA && (PINx == 9 || PINx == 10)) {
      // Sélection AF7 pour USART1 (PA9, PA10)
      GPIOx->AFR[1] &= ~(0xF << ((PINx - 8) * 4));
      GPIOx->AFR[1] |= (7 << ((PINx - 8) * 4)); // AF7 = USART1
    }
    break;
 
  case ANALOG:
    optionPort = 0x03;
    break;
 
  default:
    optionPort = 0x00;
    typePull = NO_PULL;
    break;
  }
 
  // Ecriture
  GPIOx->MODER |= optionPort << (PINx * MODER_NumberBitParPin);
 
  // TYPE DE PULL (No pull, Pull Down, Pull Up)
  uint8_t optionPull = 0x00;
 
  // Ecriture type pull
  switch (typePull) {
  case NO_PULL:
    optionPull = 0x00;
    break;
  case PULL_UP:
    optionPull = 0x01;
    break;
  case PULL_DOWN:
    optionPull = 0x02;
    break;
  default:
    optionPull = 0x00;
    break;
  }
 
  // Ecriture
  GPIOx->PUPDR |= optionPull << (PINx * PUPDR_NumberBitParPin);
}
 
void onPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  GPIOx->BSRR = (1 << PINx); // set
}
 
void offPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  GPIOx->BSRR = (1 << (PINx + OFFSET_BSRR_OFF)); // reset
}
 
int readPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  return (GPIOx->IDR & (1 << PINx));
}
 
void togglePin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  // Après sa lecture, le registre BSRR reset à 0 automatiquement
  if (readPin(GPIOx, PINx)) // Lecture pin, si 1 alors eteindre
    offPin(GPIOx, PINx);
  else // Sinon allumer
    onPin(GPIOx, PINx);
}
 
void setupCarte() {
  _devices_init();
 
  // 1
  setupPin(GPIOC, 0, OUTPUT); // PC0, CS1
  setupPin(GPIOC, 1, OUTPUT); // PC1, RST1
  setupPin(GPIOC, 2, OUTPUT); // PC2, INT1
 
  // 2
  setupPin(GPIOA, 7, OUTPUT); // PA7, CS2
  setupPin(GPIOB, 1, OUTPUT); // PB1, RST2
  setupPin(GPIOC, 4, OUTPUT); // PC4, INT2
 
  // 3
  setupPin(GPIOC, 9, OUTPUT); // PC9, CS3
  setupPin(GPIOB, 6, OUTPUT); // PB6, RST3
  setupPin(GPIOB, 5, OUTPUT); // PB5, INT3
 
  // 4
  setupPin(GPIOA, 2, OUTPUT); // PA2, CS4
  setupPin(GPIOA, 1, OUTPUT); // PA1, RST4
  setupPin(GPIOA, 0, OUTPUT); // PA0, INT4
 
  // 6
  setupPin(GPIOA, 4, OUTPUT); // PA4, CS6
 
  // Ecran
  _ecran_init();
 
  // FPGA
  setupPin(GPIOB, 0, OUTPUT); // PB0, CS_FPGA
 
  // LEDs
  setupPin(GPIOB, 8, OUTPUT); // PB8, LED1
  setupPin(GPIOA, 6, OUTPUT); // PA6, LED2
  setupPin(GPIOB, 7, OUTPUT); // PB7, LED3
 
  // BTNs
  setupPin(GPIOC, 12, INPUT); // PC12, SW_1
  setupPin(GPIOB, 11, INPUT); // PB11, SW_2
  setupPin(GPIOC, 10, INPUT); // PC10, SW_3
 
  // SPI
  spiInit();
 
  // On eteint tous les RST
  onPin(GPIOC, 1); // PC1, RST1
 
  usart_init(115200);
}
</syntaxhighlight>gpio.h :<syntaxhighlight lang="c">
#ifndef GPIO_H
#define GPIO_H
 
#include "../../../00-lib/stm32f410rx.h"
 
typedef enum {
  INPUT,
  OUTPUT,
  ALTERNATE_FUNCTION,
  ALTERNATE_FUNCTION_USART,
  ANALOG,
} portModeRegister;
 
typedef enum {
  NO_PULL,
  PULL_UP,
  PULL_DOWN,
} portPullUpPullDownRegister;
 
#define CLOCK_MHZ 16 // HSI = 16MHz Cf STM32F410 datasheet p82/142
 
void setupPin(GPIO_TypeDef *GPIOx, uint8_t PINx, portModeRegister portMode);
int readPin(GPIO_TypeDef *GPIOx, uint8_t PINx);
void offPin(GPIO_TypeDef *GPIOx, uint8_t PINx);
void onPin(GPIO_TypeDef *GPIOx, uint8_t PINx);
void togglePin(GPIO_TypeDef *GPIOx, uint8_t PINx);
 
void setupCarte();
 
#endif
</syntaxhighlight>
 
===== Ordonnanceur =====
Cette librairie ainsi que TASK sont les plus importantes car elles étaient les plus difficiles à implémenter. Aucun étudiant n'avait déjà travailler sur un ordonnanceur sur une architecture ARM et peu d'information sont présentes sur internet sur ce sujet. Les professeurs m'ont confiés ce sujet afin que je puisse aider à l'évolution du module pico ordinateur avec peut être plus de microcontrôleur sur ARM pour les prochaines années.
 
C'était donc un défi très intéressant de réussir à coder les primitives d'un ordonnanceur sur ARM.
 
ordonnanceur.c :<syntaxhighlight lang="c">
#include "ordonnanceur.h"
 
uint32_t g_tick_count = 0;
uint32_t INCREMENT_TIMER = 0;
 
extern TCB_t *current_task_ptr;
 
/* ------------------ Fonctions interne ------------------ */
uint32_t _get_psp_addr(void) {
  if (!current_task_ptr)
    return 0;
  return (uint32_t)current_task_ptr->psp_addr;
}
 
void _save_psp_addr(uint32_t addr) {
  if (!current_task_ptr)
    return;
  current_task_ptr->psp_addr = (uint32_t *)addr;
}
 
void _setupTimer5(uint32_t ms) {
  INCREMENT_TIMER = ms;
  // Choix TIM5 car gestion d'un timer simple
 
  // ACTIVATION DE L'HORLOGE TIM5 SPECIFIQUE
  RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;
 
  // Prescaler Register
  TIM5->PSC = (CLOCK_MHZ - 1); // Diviser par Clock pour avoir 1 MHz
 
  // ARR : Auto Reload Register
  TIM5->ARR = (ms * 1000) - 1; // 1/1MHz * 1000 devient des millisecondes
  // valeur à laquelle le timer reset et déclenche une interruption Cf p341
 
  // Counter Register
  TIM5->CNT = 0; // On commencer a compter à 0
 
  // DMA/Interrupt enable register
  TIM5->DIER |= TIM_DIER_UIE; //  Update interrupt enable
 
  // Activer TIM5 en mode compteur
  TIM5->CR1 |= TIM_CR1_CEN;
 
  NVIC_SetPriority(USART1_IRQn, 0x01);
  NVIC_SetPriority(TIM5_IRQn, 0x10);
  NVIC_SetPriority(PendSV_IRQn, 0x3);
 
  NVIC_EnableIRQ(TIM5_IRQn);
  NVIC_EnableIRQ(PendSV_IRQn);
}
 
void TIM5_IRQHandler(void) {
  // Clear flag
  TIM5->SR &= ~TIM_SR_UIF;
 
  g_tick_count += INCREMENT_TIMER;
  _unblock_tasks();
 
  // Déclenche PendSV (switch context)
  SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}
 
// Bascule MSP à PSP
__attribute__((naked)) void launch_os(void) {
  // Initialisation PSP avec la pile de la tâche courante
  __asm volatile("PUSH {LR}");        // Sauvegarde LR (adresse du main() )
  __asm volatile("BL _get_psp_addr"); // Appelle _get_psp_addr(), retourne le PSP de la tâche courante dans r0
  __asm volatile("MSR PSP,R0");      // Met à jour le PSP avec la nouvelle valeur
  __asm volatile("POP {LR}");        // Restaure LR sauvegardé avant les appels BL
 
  // Bascule du Stack Pointer actif de MSP vers PSP
  __asm volatile("MOV R0,#0X02");  // Selection du PSP en Thread mode (CONTROL.SPSEL = 1)
  __asm volatile("MSR CONTROL,R0"); // Mise à jour du registre CONTROL
  __asm volatile("CPSIE I");        // Active les IRQ
  __asm volatile("BX LR");          // Retour à l'appelant en utilisant désormais le PSP
}
 
// Quand PendSV est executé, le systeme enregistre le contexte de la tâche courante automatiquement.
// Ici, il est utilisé en naked donc on enregistre à la mano.
__attribute__((naked)) void PendSV_Handler(void) {
  SAVE_REGISTERS();
 
  // PC saute à l'adresse
  __asm volatile("BL _scheduler");
 
  RESTORE_REGISTERS()
}
 
void init_os(void) {
  __disable_irq();
  _setupTimer5(1);
  _init_tasks();
}
 
</syntaxhighlight>ordonnanceur.h :<syntaxhighlight lang="c">
#ifndef ORDONNANCEUR_H
#define ORDONNANCEUR_H
 
#include "../GPIO/gpio.h"
#include "../TASK/task.h"
 
 
#define SAVE_REGISTERS() \
    __asm volatile("MRS r0, PSP                @ r0 = PSP courant\n\t"  \
                  "STMDB r0!, {r4-r11}        @ Sauvegarde registre R4 à R11 sur la pile PSP\n\t"  \
                  "PUSH {LR}                  @ Sauvegarde LR sur la pile MSP avant BL\n\t" \
                  "BL _save_psp_addr            @ Appelle save_psp_addr(r0) pour mémoriser le PSP\n\t");
 
#define RESTORE_REGISTERS() \
    __asm volatile("BL _get_psp_addr            @ Appelle get_psp_addr(), retourne le PSP de la tâche suivante dans r0\n\t"\
        "LDMIA r0!, {r4-r11}                    @ Restaure R4 à R11 depuis la pile de la nouvelle tâche\n\t"\
        "MSR PSP, r0                            @ Met à jour le PSP avec la nouvelle valeur\n\t"\
        "POP {LR}                              @ Restaure LR sauvegardé avant les appels BL\n\t"\
        "BX LR                                  @ Retour d'exception : sortie de PendSV vers la tâche sélectionnée\n\t");
 
/* ------------------ Function Prototypes ------------------ */
void init_os(void);
void launch_os(void);
 
#endif
</syntaxhighlight>
 
===== Tâche =====
Contrairement à la gestion des tâches sur la carte shield, ici nous avons une gestion dynamique avec une liste chaînée (vu au semestre 6 avec M. FORGET).
 
task.c : <syntaxhighlight lang="c">
#include "task.h"
 
#include <stddef.h>
#include <stdlib.h>
 
#define DUMMY_XPSR 0x01000000 // xPSR (32bits), 24eme bit indique thumb mode sinon crash
 
/* ------------------ Variables globales ------------------ */
TCB_t *task_list_head = NULL;  // tête de liste chaînée
TCB_t *current_task_ptr = NULL; // tâche courante
 
/* ------------------ Fonctions interne ------------------ */
// Initialise les piles de chaque taches
void _init_task_stack(uint32_t **psp_addr, void (*task_handler)(void)) {
  uint32_t *pPSP = *psp_addr + STACK_SIZE;
 
  *(--pPSP) = DUMMY_XPSR;            // xPSR
  *(--pPSP) = (uint32_t)task_handler; // PC
  *(--pPSP) = 0xFFFFFFFD;            // LR, retour en Thread mode avec PSP
 
  // Initialisation registres R0-R12
  for (int i = 0; i < 13; i++) {
    *(--pPSP) = 0;
  }
 
  *psp_addr = pPSP; // Met à jour l'adresse PSP dans la TCB
}
 
// Sécurité dans le cas où il n'y aurait plus aucune tâche
void _idle_task(void) {
  while (1)
    ;
}
 
void _add_task_to_list(TCB_t *new_task) {
  // Sécurité : la liste ne doit jamais être vide, _idle_task doit exister
  if (task_list_head == NULL) {
    // Créer automatiquement _idle_task
    TCB_t *idle = (TCB_t *)malloc(sizeof(TCB_t));
    if (!idle)
      return;
 
    idle->psp_addr = (uint32_t *)malloc(STACK_SIZE * sizeof(uint32_t));
    if (!idle->psp_addr) {
      free(idle);
      return;
    }
 
    idle->current_state = TASK_READY_STATE;
    idle->block_count = 0;
    idle->task_handler = _idle_task;
    _init_task_stack(&idle->psp_addr, _idle_task);
 
    task_list_head = idle;
    idle->next = idle;      // liste circulaire
    current_task_ptr = idle; // pointe sur idle
  }
 
  // Ajout à la fin de la liste
  TCB_t *tmp = task_list_head;
  while (tmp->next != task_list_head) {
    tmp = tmp->next;
  }
  tmp->next = new_task;
  new_task->next = task_list_head;
}
 
/* ------------------ Fonctions externe ------------------ */
// update_next_task
void _scheduler(void) {
  if (!current_task_ptr) {
    current_task_ptr = task_list_head; // première tâche
  } else {
    TCB_t *start = current_task_ptr;
    do {
      current_task_ptr = current_task_ptr->next;
      if (current_task_ptr->current_state == TASK_READY_STATE)
        return;
    } while (current_task_ptr != start);
 
    // Si aucune tâche prête, idle
    current_task_ptr = task_list_head; // ou idle task
  }
}
 
// Débloque les taches endormi
void _unblock_tasks(void) {
  if (!task_list_head)
    return;
 
  TCB_t *tmp = task_list_head;
  do {
    if (tmp->current_state == TASK_BLOCKED_STATE && tmp->block_count <= g_tick_count) {
      tmp->current_state = TASK_READY_STATE;
    }
    tmp = tmp->next;
  } while (tmp != task_list_head);
}
 
// Fonction qui endors une tache le temps précisé
void task_delay(uint32_t ms) {
  __disable_irq();
 
  if (current_task_ptr && current_task_ptr->task_handler != _idle_task) { // On touche pas à idle c'est une securite
    current_task_ptr->block_count = g_tick_count + ms;
    current_task_ptr->current_state = TASK_BLOCKED_STATE;
 
    // Déclenche PendSV pour basculement de tâche
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
  }
 
  __enable_irq();
}
 
void add_task(void (*task_handler)(void)) {
  // Allocation TCB
  TCB_t *new_task = (TCB_t *)malloc(sizeof(TCB_t));
  if (!new_task)
    return; // échec allocation
 
  // Allocation pile
  new_task->psp_addr = (uint32_t *)malloc(STACK_SIZE * sizeof(uint32_t));
  if (!new_task->psp_addr) {
    free(new_task);
    return;
  }
 
  // Initialisation TCB
  new_task->current_state = TASK_READY_STATE;
  new_task->block_count = 0;
  new_task->task_handler = task_handler;
 
  // Initialisation pile PSP
  _init_task_stack(&new_task->psp_addr, task_handler);
 
  // Ajouter à la liste chaînée (à la fin)
  _add_task_to_list(new_task);
}
 
void _init_tasks(void) {
  // Initialise la liste de tâches avec _idle_task
  task_list_head = NULL;
  current_task_ptr = NULL;
 
  // Crée automatiquement _idle_task
  add_task(_idle_task);
}
 
</syntaxhighlight>task.h : <syntaxhighlight lang="c">
#ifndef TASK_H
#define TASK_H
 
#include <stdint.h>
#include "../GPIO/gpio.h"
 
/* ------------------ Types ------------------ */
typedef enum {
    TASK_READY_STATE = 0,
    TASK_BLOCKED_STATE = 1,
} state_task;
 
typedef struct TCB{
  uint32_t *psp_addr;
  void (*task_handler)(void);
  uint32_t block_count;
  state_task current_state;
 
  struct TCB *next;
} TCB_t;
 
extern uint32_t g_tick_count;
 
/* ------------------ TASK ------------------ */
#define IDLE_STACK_START 0x20001000
#define SCHED_STACK_START 0x20006000
#define STACK_SIZE        256
 
// Fonction pour dépendance ordonnanceur.c
void _scheduler(void);
void _unblock_tasks(void);
void _init_tasks(void);
 
// Fonction à partager à l'utilisateur
void task_delay(uint32_t ms);
void add_task(void (*task_handler)(void));
 
#endif
</syntaxhighlight>
 
===== SPI =====
spi.c : <syntaxhighlight lang="c">
#include "./spi.h"
 
#define AFR_NumberBitParPin 0x4 //  Cf RM0401 p155/763
#define AFR_OFFSET_HIGH 8      //  Cf RM0401 p155/763
 
#define SPI_AFR 0b0101 // SPI sur AF5, Cf RM0401 p143/763
 
void spi_cs_on(GPIO_TypeDef *GPIOx, uint8_t PINx) { offPin(GPIOx, PINx); }
void spi_cs_off(GPIO_TypeDef *GPIOx, uint8_t PINx) { onPin(GPIOx, PINx); }
 
volatile uint8_t spi_lock;
 
void spiInit() {
  // SPI configuration instruction Cf RM0401 p682/763
 
  // STEP 1: Write proper GPIO registers: Configure GPIO for MOSI, MISO and SCK
  // pins.
 
  // PB15, MOSI
  setupPin(GPIOB, 15, ALTERNATE_FUNCTION);
  GPIOB->AFR[1] &= ~(0b1111 << ((15 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin)); // Clear
  GPIOB->AFR[1] |= SPI_AFR << ((15 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin);  // AFR[1] = AFRH
 
  // PB14, MISO
  setupPin(GPIOB, 14, ALTERNATE_FUNCTION);
  GPIOB->AFR[1] &= ~(0b1111 << ((14 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin)); // Clear
  GPIOB->AFR[1] |= SPI_AFR << ((14 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin);  // AFR[1] = AFRH
 
  // PB13, SCK
  setupPin(GPIOB, 13, ALTERNATE_FUNCTION);
  GPIOB->AFR[1] &= ~(0b1111 << ((13 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin)); // Clear
  GPIOB->AFR[1] |= SPI_AFR << ((13 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin);  // AFR[1] = AFRH
 
  // STEP 2 : Write to the SPI_CR1 register:
 
  // ACTIVER L'HORLOGE AVANT TOUT SINON NE MARCHE
  RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
 
  SPI2->CR1 = 0; // Reset tout
 
  // Cf RM0401 page 711/763 le tableau des états
 
  // LES SPECS DU SPI
  // https://learn.sparkfun.com/tutorials/using-the-serial-7-segment-display/all
 
  // a) Configure the serial clock baud rate using the BR[2:0] bits (Note: 3).
  SPI2->CR1 |= 0b101 << SPI_CR1_BR_Pos; // 101 : fPCLK/64
 
  // fPCLK /32 fait 250kHz car fpclk = 8MHz et
  // on à 250kHz maximum clock, cf sparkfun spec
  // On prend alors en dessous car sinon des
  // erreurs viennent se glisser pendant l'envoie
 
  //  b) Configure the CPOL and CPHA bits combination to define one of the four
  //  relationships between the data transfer and the serial clock. (Note: 2 -
  //  except the case when CRC is enabled at TI mode).
  SPI2->CR1 &= ~(1 << SPI_CR1_CPOL_Pos); // 0 : 0 when idle
  // And, data is clocked in on the rising edge of the clock (when it goes from
  // 0V to 5V).
 
  SPI2->CR1 &= ~(1 << SPI_CR1_CPHA_Pos); // 0 :
  // first clock transition is first data capture edge
 
  // c) Select simplex or half-duplex mode by configuring RXONLY or BIDIMODE and
  // BIDIOE (RXONLY and BIDIMODE can't be set at the same time).
  SPI2->CR1 &= ~(1 << SPI_CR1_RXONLY_Pos); // 0 : full-duplex
 
  // d) Configure the LSBFIRST bit to define the frame format (Note: 2).
  SPI2->CR1 &= ~(1 << SPI_CR1_LSBFIRST_Pos); // 0 : MSB transmitted first
 
  // e) Configure the CRCEN and CRCEN bits if CRC is needed (while SCK clock
  // signal is at idle state).
  SPI2->CR1 &= ~(1 << SPI_CR1_CRCEN_Pos); // 0: CRC calculation disabled
 
  // f) Configure SSM and SSI (Note: 2).
  // When the SSM bit is set, the NSS pin input is replaced with the value from
  // the SSI
  SPI2->CR1 |= (1 << SPI_CR1_SSM_Pos); // Software slave management
  SPI2->CR1 |= (1 << SPI_CR1_SSI_Pos); //
 
  // g) Configure the MSTR bit (in multimaster NSS configuration, avoid conflict
  // state on NSS if master is configured to prevent MODF error).
  SPI2->CR1 |= 1 << SPI_CR1_MSTR_Pos; // 1 : Master configuration
 
  // Data frame format
  SPI2->CR1 &= ~(1 << SPI_CR1_DFF_Pos); // 0: 8-bit data frame format is
                                        // selected for transmission/reception
  // Enable SPI
  SPI2->CR1 |= 1 << SPI_CR1_SPE_Pos; // 1 : Peripheral enabled
}
 
uint8_t spi_write(uint8_t data, GPIO_TypeDef *CS_GPIOx, uint8_t CS_PINx) {
  uint8_t dataRecu;
 
  while (spi_lock)
    ;
  spi_lock = 1;
 
  // Activer CS
  spi_cs_on(CS_GPIOx, CS_PINx);
 
  // Attendre que TXE soit prêt
  while (!(SPI2->SR & SPI_SR_TXE))
    ;
 
  // Envoyer la donnée
  SPI2->DR = data;
 
  // Attendre que le buffer RXNE soit plein pour lire et vider
  while (!(SPI2->SR & SPI_SR_RXNE))
    ;
  dataRecu = SPI2->DR;
 
  // Désactiver CS
  spi_cs_off(CS_GPIOx, CS_PINx);
 
  spi_lock = 0;
  return dataRecu;
}
 
</syntaxhighlight>spi.h : <syntaxhighlight lang="c">
#pragma once
 
#include <stdint.h>
#include "../GPIO/gpio.h"
 
void spiInit();
void spi_cs_on(GPIO_TypeDef *GPIOx, uint8_t PINx);
void spi_cs_off(GPIO_TypeDef *GPIOx, uint8_t PINx);
 
uint8_t spi_write(uint8_t data, GPIO_TypeDef *CS_GPIOx, uint8_t CS_PINx);
</syntaxhighlight>
 
===== USART =====
Bibliothèque permettant de gérer proprement l'UART et afficher et lire des caractères via le port série d'un PC.
 
usart.c : <syntaxhighlight lang="c">
#include "usart.h"
#include "../GPIO/gpio.h"
 
#define RX_BUFFER_SIZE 64
 
volatile char rx_buffer[RX_BUFFER_SIZE];
volatile uint8_t rx_head = 0;
volatile uint8_t rx_tail = 0;
 
void usart_init(uint32_t baudrate) {
  setupPin(GPIOA, 9, ALTERNATE_FUNCTION_USART);  // PA9 => TX
  setupPin(GPIOA, 10, ALTERNATE_FUNCTION_USART); // PA10 => RX
 
  // Evidemment on active l'horloge du bus
  RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
 
  // Procedure (Cf STM32F410 p.627/763):
  // 2. Program the M bit in USART_CR1 to define the word length.
  USART1->CR1 = 0; // M=0, 1 Start bit, 8 Data bits, n Stop bit
 
  // 3. Program the number of stop bits in USART_CR2.
  USART1->CR2 = 0;
 
  // 4. Select DMA enable (DMAR) in USART_CR3 if multibuffer communication is to take
  // place. Configure the DMA register as explained in multibuffer communication. STEP 3
  USART1->CR3 = 0;
 
  // 5. Select the desired baud rate using the baud rate register USART_BRR
  uint32_t usartclk = 16000000;      // APB2 ~16MHz (Nucleo F4)
  USART1->BRR = usartclk / baudrate; // BRR : baudrate = fclk / USARTDIV
 
  // 6. Set the RE bit USART_CR1. This enables the receiver that begins searching for a start
  // bit.
  USART1->CR1 |= USART_CR1_PS; // Parity selection, 0 = Even parity
 
  // 1. Enable the USART by writing the UE bit in USART_CR1 register to 1.
  // Bon la datasheet dis etape 1 mais faut vraiment le faire à la fin l'activation sinon marche pas
  USART1->CR1 |= USART_CR1_TE | USART_CR1_RE; // TX & RX
 
  USART1->CR1 |= USART_CR1_RXNEIE; // activer interruption RX
 
  USART1->CR1 |= USART_CR1_UE; // USART
 
  NVIC_EnableIRQ(USART1_IRQn);
}
 
void usart_send_char(char c) {
  while (!(USART1->SR & USART_SR_TXE))
    ;
  USART1->DR = (c & 0xFF);
}
 
void usart_print(char *s) {
  while (*s) { // Tant que le caractère != '\0'
    usart_send_char(*s);
    s++;
  }
}
 
void USART1_IRQHandler() {
  if (USART1->SR & USART_SR_RXNE) {
    char c = USART1->DR & 0xFF;
    uint8_t next = (rx_head + 1) % RX_BUFFER_SIZE;
 
    if (next != rx_tail) { // buffer pas plein
      rx_buffer[rx_head] = c;
      rx_head = next;
    }
  }
}
 
int usart_buffer_available() {
  return (rx_head != rx_tail);
}
 
char usart_read() {
  if (rx_head == rx_tail)
    return 0; // rien dispo
 
  char c = rx_buffer[rx_tail];
  rx_tail = (rx_tail + 1) % RX_BUFFER_SIZE;
  return c;
}
 
</syntaxhighlight>usart.h : <syntaxhighlight lang="c">
#ifndef USART_H
#define USART_H
 
#include "../../../00-lib/stm32f410rx.h"
 
void usart_init(uint32_t baudrate);
void usart_send_char(char c);
void usart_print(char *s);
void usart_print_c(char c);
 
int usart_buffer_available(void);
char usart_read(void);
 
#endif
</syntaxhighlight>
 
===== Substitute =====
Ce code à pour but de simplifier l'implémentation de la carte clavier ou la carte écran à moyen et long terme.
printf.h :<syntaxhighlight lang="c">
#ifndef PRINTF_H
#define PRINTF_H
 
#include "../USART/usart.h"
 
#define PRINT_STRING(str)        usart_print((char*)str)
#define PRINT_CHAR(c)        usart_send_char(c)
 
#define GETCHAR()          usart_read()
#define WAITCHAR()          !usart_buffer_available()
 
#endif
</syntaxhighlight>Grâce à cette fonction macro on pourra changer facilement la dépendance USART en combinaison carte clavier et/ou écran.
 
== Carte fille Clavier ==
 
=== Hardware ===
 
==== Boutons utilisés ====
Nous voulions implémenter un clavier à touches mécaniques. Monsieur Redon avait des switchs qui convenait donc nous n'avions pas besoin d'en recommander.
 
Les switchs sont de la marque KAILH :
[[Fichier:Boite Kailh.jpg|alt=Boite Kailh|vignette|center|Boite Kailh Switch]]
<p style="clear: both;" />
 
Petit bémol : les '''sockets hot-swap''' ne correspondaient pas à nos boutons. Comme on peut le voir sur la photo de droite, il existe '''deux types de sockets''' pour nos modules hot-swap. Nous avons donc dû commander les modèles adaptés.
 
[[Fichier:Kailh Hot swap socket.png|left|300px|vignette|Kailh Hot swap socket|300x300px]]
[[Fichier:Bouton kailh comparaison hot swap.jpg|right|vignette|300x300px|Bouton kailh comparaison hot swap socket]]
<p style="clear: both;" />
 
L’intérêt de ces modules est de '''pouvoir insérer ou retirer les boutons à volonté''', sans qu’ils soient soudés directement au PCB — ce sont les sockets qui, eux, sont soudés. Cette solution présente plusieurs avantages : elle '''facilite la réutilisation des boutons''' d’un projet à un autre et '''améliore la réparabilité''' du clavier.
 
==== Concevons un clavier ! ====
Notre clavier doit comporter '''62 touches''', conformément au '''format standard ISO 60 %''', et sera capable d’'''assurer l’ensemble des combinaisons de touches attendues pour un clavier moderne en 2025'''.
 
Afin de customiser notre clavier, on se rend sur le site [https://www.keyboard-layout-editor.com/#/ keyboard-layout-editor] .
 
Nous pouvons partir d'un modèle de base ou alors d'un preset :
[[Fichier:Site keyboard layout .png|centré|vignette|534x534px|Site keyboard layout preset ISO 60%]]
 
Nous nous sommes alors inspirés des '''claviers disponibles sur le marché''' afin d’adopter un placement des touches conforme aux dispositions les plus courantes.
 
[[Fichier:Keyboard-layout.jpg|centré|vignette|575x575px|Keyboard layout]]
<p style="clear: both;" />
 
L’utilisation de cet outil présente plusieurs intérêts : elle permet d’'''imaginer et définir la disposition du clavier''', de '''le découper en lignes et colonnes''' afin de concevoir la '''matrice de touches''', et enfin d’'''identifier les bonnes empreintes''' à utiliser sur le futur PCB grâce au '''sommaire illustré ci-dessous'''.
 
[[Fichier:Summary keyboard layout.png|centré|vignette|399x399px|Summary keyboard layout]]
 
<p style="clear: both;" />
 
Ce sommaire indique la '''taille indicative de chaque type de touche''' ainsi que '''le nombre de touches associées''' à chacune d’elles. Le "coloriage" est utile pour voir visuellement quelle touche correspond à quelle taille.
 
On peut également sauvegarder notre configuration en exportant sous format "json". Via ce format on peut utiliser une extension de kicad qui se prénomme "'''Keyboard footprints placer'''" et qui permet de placer automatiquement les boutons si on les intancie dans le bon ordre (exemple : bouton 1 => SW1 , etc...). L'outil est un peu capricieux mais fait gagner un temps précieux sur le routage.
 
<p style="clear: both;" />
 
==== Schématique ====
'''<u>Notre carte fille comporte plusieurs éléments :</u>'''
# Le microcontrôleur ATMega32U4 avec un cristal de 16 MHz, des capacités de découplage et une ferrite (Cf AVR042) ;
# L'USB pour la programmation et l'alimentation pendant la phase programmation du projet ;
# Le connecteur ISP ;
# Les boutons RST et HWB ;
# Le connecteur SPI pour la communication avec la carte mère ;
# La led pour l'alimentation de la carte ;
# La led pour l'état du clavier (rôle ?) ;
# La matrice de touches évidemment ;
# Des mounting holes.
 
Remarque : Pas de leds RGB, pas assez de pins et nous ne voulions pas nous éparpiller sur trop d'idées (sujet evoqué avec Monsieur Boé).
 
[[Fichier:Clavier schematique.pdf|center|700px|alt=Clavier schematique|vignette|Clavier schematique]]
 
==== Vue 3D ====
[[Fichier:Keyboard 3D up v2.png|gauche|vignette|652x652px|Keyboard 3D up]]
[[Fichier:Keyboard 3D back v2.png|vignette|649x649px|Keyboard 3D back]]
<p style="clear: both;" />
 
==== Brasure ====
Nous avons soudé le strict minimum sur notre carte pour le faire fonctioner avant tout puisque que nous souhaitons trouver les éventuelles anomalies de soudure ou de conception avant chaque grosse étape. Sur la PCB rouge et la 1ère pcb verte tout est ok.
 
[[Fichier:Clavier brasé.jpg|500px|alt=clavier brasé|vignette|clavier brasé|centré]]La seconde PCB verte en revanche n'est pas reconnu. On teste les différentes connexions au multimètre. Le 5V est bien là. On teste alors le quartz à l'oscilloscope qui semble donner du bruit dans le cas de notre pcb dysfonctionnelle. Cependant après avoir changé le quartz, le problème est toujours présent. On finit alors par se rendre compte que l'on a inversé une capacité avec une resistance, le problème est donc résolu rapidement.<p style="clear: both;" />
 
[[Fichier:Test oscillo.jpg|left|400px|alt=test_oscillo|vignette|test_oscillo]]
[[Fichier:Oscillo vert.jpg|right|300px|alt=oscillo_vert|vignette|oscillo_vert]]
[[Fichier:Oscillo rouge.jpg|right|300px|alt=oscillo_rouge|vignette|oscillo_rouge]]
 
<p style="clear: both;" />
 
=== Software ===
 
==== Test Led ====
Afin de vérifier que notre clavier fonctionne, on fait un test afin de faire clignoter nos deux leds : led d'alimentation et led pour Cap Lock (qui nous servira par la suite pour savoir si notre carte est en mode majuscule ou non).
 
Remarque : la fonction setupPin est la même que celle présentée dans la section ordonnanceur de la carte shield.<syntaxhighlight lang="c">
#define F_CPU 16000000UL
 
#define LEDs_PORT PORTE
#define LEDs_DDR DDRE
#define LEDs_PIN PINE
#define LED_CapsLock PE6
 
void setupHardware() {
  setupClock();
  // Leds
  setupPin(&LEDs_PORT, &LEDs_DDR, LED_CapsLock, OUTPUT);
 
  // Permet de liberer le portF pour utiliser les boutons !
  MCUCR |= (1 << JTD); // 1ère écriture
  MCUCR |= (1 << JTD); // Désactiver JTAG (2ème écriture obligatoire !)
}
 
int main() {
  setupHardware();
  while (1) {
      LEDs_PORT |= (1 << LED_CapsLock); // toggle LED
      _delay_ms(500);
      LEDs_PORT &= ~(1 << LED_CapsLock); // toggle LED
      _delay_ms(500);
  }
    return 0;
}
 
</syntaxhighlight>
 
==== Détection de notre matrice de boutons ====
 
On créer un fichier qui pourra être facilement importé dans nos différents projets afin d'avoir une détection de touches portable pour la suite : la LUFA.
 
Voici alors clavier.c :<syntaxhighlight lang="c">
#include "clavier.h"
 
#include "../lib/HARDWARE/hardware.h"
#include <avr/io.h>
 
// --------- Colonnes ---------
volatile uint8_t *col_ports[TOTAL_COL] = {
    [0 ... 5] = &PORTF,  // COL0 à COL5
    [6 ... 7] = &PORTC,  // COL6 à COL7
    [8 ... 10] = &PORTB,  // COL8 à COL10
    [11 ... 13] = &PORTD, // COL11 à COL13
};
 
volatile uint8_t *col_ddr[TOTAL_COL] = {
    [0 ... 5] = &DDRF,  // COL0 à COL5
    [6 ... 7] = &DDRC,  // COL6 à COL7
    [8 ... 10] = &DDRB,  // COL8 à COL10
    [11 ... 13] = &DDRD, // COL11 à COL13
};
 
volatile uint8_t *col_pins_reg[TOTAL_COL] = {
    [0 ... 5] = &PINF,  // COL0 à COL5
    [6 ... 7] = &PINC,  // COL6 à COL7
    [8 ... 10] = &PINB,  // COL8 à COL10
    [11 ... 13] = &PIND, // COL11 à COL13
};
 
uint8_t col_pins[TOTAL_COL] = {0, 1, 4, 5, 6, 7, 7, 6, 6, 5, 4, 7, 6, 4};
 
// --------- Lignes ---------
volatile uint8_t *row_ports[TOTAL_ROW] = {
    [0 ... 4] = &PORTD,
};
 
volatile uint8_t *row_ddr[TOTAL_ROW] = {
    [0 ... 4] = &DDRD,
};
 
uint8_t row_pins[TOTAL_ROW] = {5, 3, 2, 1, 0};
 
uint8_t key_state[TOTAL_COL][TOTAL_ROW] = {0};
 
void init_matrix_button(void) {
  // Configuration colonnes en entrée avec pull-up
  for (uint8_t c = 0; c < TOTAL_COL; c++) {
    setupPin(col_ports[c], col_ddr[c], col_pins[c], INPUT_PULL_UP);
  }
 
  // Configuration ligne en sortie
  for (uint8_t r = 0; r < TOTAL_ROW; r++) {
    setupPin(row_ports[r], row_ddr[r], row_pins[r], OUTPUT);
    onPin(row_ports[r], row_pins[r]); // mettre toutes les lignes à 1 pour les desactiver
  }
}
 
void scan() {
  for (uint8_t r = 0; r < TOTAL_ROW; r++) {
    offPin(row_ports[r], row_pins[r]); // activer ligne (LOW)
 
    for (uint8_t c = 0; c < TOTAL_COL; c++)
      key_state[c][r] = !(*col_pins_reg[c] & (1 << col_pins[c]));
 
    onPin(row_ports[r], row_pins[r]); // désactiver ligne (HIGH)
  }
}
 
</syntaxhighlight>Le scan se fait facilement une fois la logique assimilée... 
 
Et clavier.h : <syntaxhighlight lang="c">
#ifndef CLAVIER_H
#define CLAVIER_H
 
#include "keyswitch.h"
#include <stdint.h>
#include "clavier_conversion.h"
 
extern uint8_t key_state[TOTAL_COL][TOTAL_ROW];
 
void init_matrix_button(void);
void scan(void);
 
#endif
</syntaxhighlight>Et ensuite, nous avons plein de define afin de lire chaque bouton individuellement au lieu d'appeler un tableau (pas intuitif pour l'utilisateur) dans un fichier keyswitch.h :<syntaxhighlight lang="c">
#ifndef KEYSWITCH_H
#define KEYSWITCH_H
 
#include <stdint.h>
 
#define TOTAL_KEYSWITCH 62
#define TOTAL_COL 14
#define TOTAL_ROW 5
 
 
// COL0 PF0 | COL1 PF1 | COL2 PF4 | COL3  PF5 | COL4  PF6 | COL5  PF7 | COL6  PC7 |
// COL7 PC6 | COL8 PB6 | COL9 PB5 | COL10 PB4 | COL11 PD7 | COL12 PD6 | COL13 PD4
typedef enum{
    COL0 = 0,
    COL1 = 1,
    COL2 = 2,
    COL3 = 3,
    COL4 = 4,
    COL5 = 5,
    COL6 = 6,
    COL7 = 7,
    COL8 = 8,
    COL9 = 9,
    COL10 = 10,
    COL11 = 11,
    COL12 = 12,
    COL13 = 13,
} COLs;
 
// ROW0 PD5 | ROW1 PD3 | ROW2 PD2 | ROW3 PD1 | ROW4 PD0
typedef enum{
    ROW0 = 0,
    ROW1 = 1,
    ROW2 = 2,
    ROW3 = 3,
    ROW4 = 4,
} ROWs;
 
extern uint8_t key_state[TOTAL_COL][TOTAL_ROW];
 
#define btn1  key_state[COL0][ROW0]
#define btn2  key_state[COL1][ROW0]
#define btn3  key_state[COL2][ROW0]
#define btn4  key_state[COL3][ROW0]
#define btn5  key_state[COL4][ROW0]
#define btn6  key_state[COL5][ROW0]
#define btn7  key_state[COL6][ROW0]
#define btn8  key_state[COL7][ROW0]
#define btn9  key_state[COL8][ROW0]
#define btn10 key_state[COL9][ROW0]
#define btn11 key_state[COL10][ROW0]
#define btn12 key_state[COL11][ROW0]
#define btn13 key_state[COL12][ROW0]
#define btn14 key_state[COL13][ROW0]
 
#define btn15 key_state[COL0][ROW1]
#define btn16 key_state[COL1][ROW1]
#define btn17 key_state[COL2][ROW1]
#define btn18 key_state[COL3][ROW1]
#define btn19 key_state[COL4][ROW1]
#define btn20 key_state[COL5][ROW1]
#define btn21 key_state[COL6][ROW1]
#define btn22 key_state[COL7][ROW1]
#define btn23 key_state[COL8][ROW1]
#define btn24 key_state[COL9][ROW1]
#define btn25 key_state[COL10][ROW1]
#define btn26 key_state[COL11][ROW1]
#define btn27 key_state[COL12][ROW1]
#define btn28 key_state[COL13][ROW1]
 
#define btn29 key_state[COL0][ROW2]
#define btn30 key_state[COL1][ROW2]
#define btn31 key_state[COL2][ROW2]
#define btn32 key_state[COL3][ROW2]
#define btn33 key_state[COL4][ROW2]
#define btn34 key_state[COL5][ROW2]
#define btn35 key_state[COL6][ROW2]
#define btn36 key_state[COL7][ROW2]
#define btn37 key_state[COL8][ROW2]
#define btn38 key_state[COL9][ROW2]
#define btn39 key_state[COL10][ROW2]
#define btn40 key_state[COL11][ROW2]
#define btn41 key_state[COL12][ROW2]
 
#define btn42 key_state[COL0][ROW3]
#define btn43 key_state[COL1][ROW3]
#define btn44 key_state[COL2][ROW3]
#define btn45 key_state[COL3][ROW3]
#define btn46 key_state[COL4][ROW3]
#define btn47 key_state[COL5][ROW3]
#define btn48 key_state[COL6][ROW3]
#define btn49 key_state[COL7][ROW3]
#define btn50 key_state[COL8][ROW3]
#define btn51 key_state[COL9][ROW3]
#define btn52 key_state[COL10][ROW3]
#define btn53 key_state[COL11][ROW3]
#define btn54 key_state[COL13][ROW3]
 
#define btn55 key_state[COL0][ROW4]
#define btn56 key_state[COL1][ROW4]
#define btn57 key_state[COL2][ROW4]
#define btn58 key_state[COL6][ROW4]
#define btn59 key_state[COL10][ROW4]
#define btn60 key_state[COL11][ROW4]
#define btn61 key_state[COL12][ROW4]
#define btn62 key_state[COL13][ROW4]
 
#endif
</syntaxhighlight>La bibliothèque à été pensée pour être facilement réadaptable et lisible pour un utilisateur souhaitant re-coder un clavier mais également pour quelqu'un ne souhaitant pas refaire l'hardware et juste coder/utiliser le clavier existant.
 
==== LUFA ====
Afin que nos touches soient reconnues comme des lettres de l'alphabet que l'on peut voir sur notre écran, on utilise la LUFA. Rien de bien compliqué, on retrouve le projet LUFA sur ce lien github : https://github.com/abcminiuser/lufa
 
On télécharge le projet et on vient extraire la librairie LUFA et l'exemple Keyboard présent dans le dossier Démo. On modifie le Makefile présent ainsi : <syntaxhighlight lang="makefile">
MES_LIBS = lib/HARDWARE/hardware.c lib/CLAVIER/clavier.c
 
MCU          = atmega32u4
ARCH        = AVR8
BOARD        = NONE
F_CPU        = 16000000
F_USB        = $(F_CPU)
OPTIMIZATION = s
TARGET      = Keyboard
SRC          = $(TARGET).c Descriptors.c $(MES_LIBS) $(LUFA_SRC_USB) $(LUFA_SRC_USBCLASS)
LUFA_PATH    = ../LUFA
CC_FLAGS    = -DUSE_LUFA_CONFIG_HEADER -IConfig/
LD_FLAGS    =
 
# Default target
all:
 
# Include LUFA-specific DMBS extension modules
DMBS_LUFA_PATH ?= $(LUFA_PATH)/Build/LUFA
include $(DMBS_LUFA_PATH)/lufa-sources.mk
include $(DMBS_LUFA_PATH)/lufa-gcc.mk
 
# Include common DMBS build system modules
DMBS_PATH      ?= $(LUFA_PATH)/Build/DMBS/DMBS
include $(DMBS_PATH)/core.mk
include $(DMBS_PATH)/cppcheck.mk
include $(DMBS_PATH)/doxygen.mk
include $(DMBS_PATH)/dfu.mk
include $(DMBS_PATH)/gcc.mk
include $(DMBS_PATH)/hid.mk
include $(DMBS_PATH)/avrdude.mk
include $(DMBS_PATH)/atprogram.mk
 
PROGRAMMER = avrdude
AVRDUDE_PORT = /dev/ttyACM0
AVRDUDE_BAUD = 115200
AVRDUDE_PROGRAMMER = avr109
 
upload: $(TARGET).hex
$(PROGRAMMER) -v -p $(MCU) -c $(AVRDUDE_PROGRAMMER) \
-P $(AVRDUDE_PORT) -b $(AVRDUDE_BAUD) -D \
-U flash:w:$(TARGET).hex:i
</syntaxhighlight>La ligne upload est nécessaire seulement si vous avez un bootloader personnalisé, auquel cas faire un simple make dfu pour téléverser.
 
Ensuite on vient ajouter nos libraries et notre code de logique aux fichiers Keyboard.c :<syntaxhighlight lang="c">
#include "./lib/CLAVIER/clavier.h"
#include "./lib/HARDWARE/hardware.h"
 
// ... je passe les détails basique du fichier
 
void SetupHardware() {
  /* Disable watchdog if enabled by bootloader/fuses */
  MCUSR &= ~(1 << WDRF);
  wdt_disable();
 
  /* Disable clock division */
  clock_prescale_set(clock_div_1);
 
  /* Hardware Initialization */
  setupHardware();
  init_matrix_button();
 
  // Initialisation USB obligatoire
  USB_Init();
}
 
// ... je passe les détails basique du fichier
 
bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t *const HIDInterfaceInfo,
                                        uint8_t *const ReportID,
                                        const uint8_t ReportType,
                                        void *ReportData,
                                        uint16_t *const ReportSize) {
 
  USB_KeyboardReport_Data_t *KeyboardReport = (USB_KeyboardReport_Data_t *)ReportData;
uint8_t UsedKeyCodes = 0;
 
  scan();
 
    // ROW0
    if (btn1)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_ESCAPE);
    if (btn2 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_1_AND_EXCLAMATION);
    if (btn3 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_2_AND_AT);
    if (btn4 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_3_AND_HASHMARK);
    if (btn5 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_4_AND_DOLLAR);
    if (btn6 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_5_AND_PERCENTAGE);
    if (btn7 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_6_AND_CARET);
    if (btn8 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_7_AND_AMPERSAND);
    if (btn9 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_8_AND_ASTERISK);
    if (btn10 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_9_AND_OPENING_PARENTHESIS);
    if (btn11 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_0_AND_CLOSING_PARENTHESIS);
    if (btn12 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_MINUS_AND_UNDERSCORE); // ) ° ]
    if (btn13 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_EQUAL_AND_PLUS); // = + }
    if (btn14) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_BACKSPACE);
 
    // ROW1
    if (btn15) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_TAB);
    if (btn16) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_A);
    if (btn17) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Z);
    if (btn18) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_E);
    if (btn19) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_R);
    if (btn20) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_T);
    if (btn21) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Y);
    if (btn22) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_U);
    if (btn23) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_I);
    if (btn24) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_O);
    if (btn25) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_P);
    if (btn26) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_OPENING_BRACKET_AND_OPENING_BRACE); // ^¨
    if (btn27) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_CLOSING_BRACKET_AND_CLOSING_BRACE); // $£¤
    if (btn28) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_ENTER);
 
    // ROW2
    if (btn29) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_CAPS_LOCK);
    if (btn30) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Q);
    if (btn31) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_S);
    if (btn32) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_D);
    if (btn33) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F);
    if (btn34) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_G);
    if (btn35) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_H);
    if (btn36) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_J);
    if (btn37) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_K);
    if (btn38) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_L);
    if (btn39) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_M);
    if (btn40) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_APOSTROPHE_AND_QUOTE); // % ù
    if (btn41) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_NON_US_HASHMARK_AND_TILDE); // µ *
 
    // ROW3
    if (btn42) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_SHIFT);
    if (btn43) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_NON_US_BACKSLASH_AND_PIPE);
    if (btn44) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_W);
    if (btn45) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_X);
    if (btn46) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_C);
    if (btn47) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_V);
    if (btn48) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_B);
    if (btn49) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_N);
    if (btn50) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN); // ,?
  if (btn51)
    KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN; // ;.
    if (btn52) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_DOT_AND_GREATER_THAN_SIGN); // :/
    if (btn53 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_SLASH_AND_QUESTION_MARK);  // !§
    if (btn54) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_RIGHT_SHIFT);
 
  // ROW4
  if (btn55) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_LEFTCTRL);
  if (btn56) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_LEFTGUI);
  if (btn57) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_ALT);
  if (btn58) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_SPACE);
  if (btn59 && !btn62) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_RIGHTALT);
  // if (btn60 && !btn62) KeyboardReport->Keyboard |= QWERTY_to_AZERTY(); // Trouver une fonction a celui ci
  if (btn61 && !btn62) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_RIGHTCTRL);
 
  // Fonction spéciale du clavier côté HARDWARE
  if(btn62){
    // Fonction F1 à F12
    if (btn2)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F1);
    if (btn3)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F2);
    if (btn4)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F3);
    if (btn5)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F4);
    if (btn6)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F5);
    if (btn7)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F6);
    if (btn8)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F7);
    if (btn9)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F8);
    if (btn10) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F9);
    if (btn11) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F10);
    if (btn12) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F11);
    if (btn13) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F12);
 
    // Déplacement
    if (btn60) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_DOWN_ARROW);
    if (btn59) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_ARROW);
    if (btn61) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_RIGHT_ARROW);
    if (btn53) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_UP_ARROW);
 
  }
 
  *ReportSize = sizeof(USB_KeyboardReport_Data_t);
  return false;
}
 
void CALLBACK_HID_Device_ProcessHIDReport(USB_ClassInfo_HID_Device_t *const HIDInterfaceInfo,
                                          const uint8_t ReportID,
                                          const uint8_t ReportType,
                                          const void *ReportData,
                                          const uint16_t ReportSize) {
 
  uint8_t *LEDReport = (uint8_t *)ReportData;
 
  if (*LEDReport & HID_KEYBOARD_LED_CAPSLOCK)
    onPin(LEDs_PORT,LED_CapsLock);
  else
    offPin(LEDs_PORT,LED_CapsLock);
 
}
 
 
</syntaxhighlight>
 
J'ai fais une petite macro (reprise de l'année dernière, projet manette) pour convertir les caractères sur un clavier AZERTY dans le fichier clavier_conversion.h :<syntaxhighlight lang="c">
#ifndef CLAVIER_CONVERSION_H
#define CLAVIER_CONVERSION_H
 
#include <../../LUFA/Drivers/USB/USB.h>
 
static inline uint8_t QWERTY_to_AZERTY(uint8_t qwerty_code) {
    switch (qwerty_code) {
        // Lettres
        case HID_KEYBOARD_SC_Q: return HID_KEYBOARD_SC_A;
        case HID_KEYBOARD_SC_W: return HID_KEYBOARD_SC_Z;
        case HID_KEYBOARD_SC_A: return HID_KEYBOARD_SC_Q;
        case HID_KEYBOARD_SC_Z: return HID_KEYBOARD_SC_W;
        case HID_KEYBOARD_SC_M: return HID_KEYBOARD_SC_SEMICOLON_AND_COLON;
        case HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN: return HID_KEYBOARD_SC_M;       
        default: return qwerty_code;
    }
}
 
static inline uint8_t AZERTY_to_QWERTY(uint8_t azerty_code) {
    switch (azerty_code) {
        // Lettres
        case HID_KEYBOARD_SC_A: return HID_KEYBOARD_SC_Q;
        case HID_KEYBOARD_SC_Z: return HID_KEYBOARD_SC_W;
        case HID_KEYBOARD_SC_Q: return HID_KEYBOARD_SC_A;
        case HID_KEYBOARD_SC_W: return HID_KEYBOARD_SC_Z;
        case HID_KEYBOARD_SC_SEMICOLON_AND_COLON: return HID_KEYBOARD_SC_M;
        case HID_KEYBOARD_SC_M: return HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN;
        default: return azerty_code;
    }
}
 
#endif
 
</syntaxhighlight>J'ai eu beaucoup de mal à trouver certains caractères mais le document présent ci-dessous m'a grandement aidé :
[[Fichier:HID Usage Tables.pdf|centré|vignette|HID Usage Tables]]
A partir de la page 89 nous avons l'ensemble des codes et détails pour certains d'entre eux. Malgré tout une touche n'as pas réussi à être mappée (je n'ai pas trouvé le code équivalent pour mon clavier AZERTY), cette touche correspond au point virgule / point (;.) (Bouton 51). Update : Dernier bouton trouvé :) Clavier entierement mappé !
 
Cependant le code reste fonctionnelle, j'y ai ajouté la possiblité de se déplacer via la touche FN qui correspond aux fonctionnalités spéciales, comme toutes les touches F1,F2,...F10,F11,F12 qui sont mappées aux touches 1,2,...,0,°,+.
 
==== Communication carte mère ====
Cette partie nécessite d'être travaillée mais servira de point de départ pour les prochaines années je l'espère.
Dans un dossier SPI à part nous avons ajouté un fichier cmd.h :<syntaxhighlight lang="c">
#ifndef CMD_H
#define CMD_H
 
#include <stdint.h>
 
#define CMD_NOCMD 0x00
#define CMD_ACK_SLAVE 0xFF
 
// --------------  Detection presence carte fille --------------
#define CMD_PING 0xAA
#define CMD_PING_REPLY 0x55
 
// --------------  Identification carte fille --------------
#define CMD_IDENTIFY 0x10
 
#define ID_KEYBOARD 0x01
#define ID_ECRAN 0x02
#define ID_SON 0x03
#define ID_RESEAU 0x04
#define ID_FPGA 0x05
 
// -------------- Specifique Keyboard --------------
#define CMD_READ_EVENT 0x20
 
// -------- Structure keycode -------- //
typedef struct {
  char key;        // lettre correspondante
  uint8_t pressed; // 1 = press, 0 = release
} key_event_t;
 
#endif
</syntaxhighlight>Il sert à simplifier l'implémentation des commandes qui sont communes à tous les groupes.
spi.c :<syntaxhighlight lang="c">
#include "spi.h"
#include <avr/io.h>
#include <avr/interrupt.h>
 
// -------- SPI avec carte mere -------- //
#define INT_PORT PORTB
#define INT_DDR  DDRB
#define INT_PIN  PB0
 
#define INT_LOW()  (INT_PORT &= ~(1 << INT_PIN))
#define INT_HIGH()  (INT_PORT |=  (1 << INT_PIN))
 
static volatile uint8_t spi_state = 0;
static volatile key_event_t current_event;
 
// -------- Buffer circulaire -------- //
#define SPI_BUFFER_SIZE 16
static volatile key_event_t buffer[SPI_BUFFER_SIZE];
static volatile uint8_t head = 0;
static volatile uint8_t tail = 0;
 
 
static inline uint8_t __bufferIsEmpty(void) {
    return head == tail;
}
 
static inline void __bufferPush(key_event_t ev) {
    uint8_t next = (head + 1) % SPI_BUFFER_SIZE;
 
    if (next == tail) {
        tail = (tail + 1) % SPI_BUFFER_SIZE; // overwrite
    }
 
    buffer[head] = ev;
    head = next;
}
 
static inline uint8_t __bufferPop(volatile key_event_t *ev) {
    if (__bufferIsEmpty())
        return 0;
 
    *ev = buffer[tail];
    tail = (tail + 1) % SPI_BUFFER_SIZE;
    return 1;
}
 
// -------- SPI -------- //
void spi_init(void) {
    /* Config SPI Slave : MISO => output, MOSI/SCK/SS => input */
    DDRB |=  (1 << PB3);  // MISO
    DDRB &= ~((1 << PB2) | (1 << PB1) | (1 << PB0)); // MOSI, SCK, SS
 
    /* SPI enable + interruption */
    SPCR = (1 << SPE) | (1 << SPIE);
 
    /* INT pin */
    INT_DDR |= (1 << INT_PIN);
    INT_HIGH();
 
    SPDR = CMD_ACK_SLAVE; // On precharge ack pour le 1er cycle, a chaque reset c'est utile
}
 
ISR(SPI_STC_vect) {
    uint8_t rx = SPDR;
    uint8_t tx = CMD_NOCMD;
 
    switch (rx) {
        case CMD_PING:
            tx = CMD_PING_REPLY;
            break;
 
        case CMD_IDENTIFY:
            tx = ID_KEYBOARD;
            break;
 
        case CMD_READ_EVENT:
            if (__bufferPop(&current_event)) {
                tx = current_event.key;
                spi_state = 1; // prochain octet = pressed
            } else {
                tx = CMD_NOCMD;
                INT_HIGH(); // RAS pour la carte mere
            }
            break;
 
        default:
            if (spi_state == 1) {
                tx = current_event.pressed;
                spi_state = 0;
 
                if (__bufferIsEmpty())
                    INT_HIGH(); // RAS pour la carte mere
            }
            break;
    }
 
    SPDR = tx; // a envoyer au prochain cycle
}
// Manque a coder dautre primitive
 
 
</syntaxhighlight>spi.h :<syntaxhighlight lang="c">
#ifndef SPI_H
#define SPI_H
 
#include "cmd.h"
 
void spi_init(void);
void spi_push_event(char key, uint8_t pressed);
 
#endif
 
</syntaxhighlight>


== Carte Fille FPGA ==
== Carte Fille FPGA ==

Version actuelle datée du 12 janvier 2026 à 13:37

Cahier des charges

L'objectif pour notre groupe est de réaliser une carte mère.

Lien GIT

Lien du git : https://gitea.plil.fr/ahouduss/SE4-Pico-B6

Carte Shield

La première étape est de réaliser un shield au cas où notre carte mère s'avérerait non fonctionnelle, afin de ne pas bloquer l'avancée des groupes des cartes filles.

Hardware

Composants

Afin de réaliser notre bouclier qui combiné à un arduino uno fera guise de carte mère, nous utilisons les composants suivants :

- Puce ATMega328-A en tant que microprocesseur

- 5 connecteurs 2*4 pour les cartes filles (clavier, écran, réseau, son) et un connecteur 2*4 pour connecter la carte mémoire.

- Des convertisseurs de niveaux logiques 5V vers 3,3V pour l'utilisation de la carte mémoire (même si il aurait été préférable de mettre la partie conversion directement sur la carte mémoire).

Schématique et vue 3D

Pico-shield_schematique
Pico-shield_schematique
CarteShield 3D
CarteShield 3D

Carte mémoire

En extension de notre shield ou de notre future carte mère, on ajoute la gestion de la mémoire avec la carte SD sur une carte mémoire distincte.

Memoire_schematic
Memoire_schematic
Memoire 3D
Memoire 3D

Brasage

On procède au brasage des cartes shield et mémoire.

cartes shield et memoire brasées
cartes shield et memoire brasées
cartes shield et memoire brasées 2
cartes shield et memoire brasées 2

Tests

Test leds

On teste les leds et on constate que notre carte shield est fonctionelle.

carte shield test leds
carte shield test leds

Test carte SD
correction_pcb
correction_pcb

On teste ensuite un programme arduino simple au préalable pour voir si la carte SD est détectée. La carte n'étant pas détectée, on regarde alors :

  1. que la connexion série est bien établie pour voir si le problème ne vient pas de l'IDE Arduino -> ce n'est pas le cas.
  2. si la carte SD est défaillante en testant le programme avec une autre carte SD. On teste aussi sur un lecteur de carte SD pour voir si elle est détectée sur le pc ce qui est le cas -> le pb ne vient pas de là non plus.
  3. on s'intéresse maintenant à l'aspect matériel, on vérifie les soudures -> toujours pas de souci particulier.
  4. schématique et routage de la carte : on s'aperçoit alors que l'on a inversé le sens du 74LVC125 de l'unité U1A pour la conversion de niveau logique du MOSI en appuyant par erreur sur le raccourci clavier x qui inverse en "miroir" le sens du composant. Le routage à été modifié sur kicad par la suite et l'erreur réparé comme on peut le voir sur la photo.

Software

Programmation carte SD

On ne programme pas la carte SD ici, on le fait directement sur la nucleo.

Ordonnanceur

Maintenant que notre shield est fonctionnel, nous pouvons réaliser notre ordonnanceur. A voir ici : https://gitea.plil.fr/ahouduss/SE4-Pico-B6/src/branch/master/03-Code/Carte_shield/ordonnanceur

Faire un ordonnanceur sur une architecture AVR est nécessaire si l'on souhaite ensuite le faire sur une architecture ARM Cortex M4 qui est bien plus complexe.

Notre ordonnanceur est structuré de la manière suivante :

  • un main.c
  • lib qui contient les fichiers nécéssaires pour le main.c
    1. Un dossier Hardware
    2. Un dossier ordonnanceur
    3. Un dossier Task
    4. Un dossier USART
  • build : un dossier qui stocke les exécutables à part

Description des fichiers et fonctions implémentées :

HARDWARE
#include "hardware.h"
#include <avr/io.h>
#include <util/delay.h>

#include "../USART/usart.h"


void setupClock() {
    // Activer possibilité de changer le prescaler
    CLKPR = (1 << CLKPCE);

    // Choix diviseur
    CLKPR = 0;  
}

void setupPin(volatile uint8_t *PORTx, volatile uint8_t *DDRx, uint8_t pin, pinmode mode) {
  switch (mode) {
  case INPUT: // Forcage pin à 0
    *DDRx &= ~(1 << pin);
    break;
  case INPUT_PULL_UP: // Forcage pin à 0
    *DDRx &= ~(1 << pin);
    *PORTx |= (1 << pin);
    break;
  case OUTPUT: // Forcage pin à 1
    *DDRx |= (1 << pin);
    break;
  }
}

int readPin(volatile uint8_t *PINx, uint8_t pin) {
  return (*PINx & (1 << pin));
}

void setupHardware(){
  setupClock();

  setupPin(LEDs_PORT, LEDs_DDR, LED_CS1, OUTPUT);
  setupPin(LEDs_PORT, LEDs_DDR, LED_CS2, OUTPUT);
  setupPin(LEDs_PORT, LEDs_DDR, LED_CS3, OUTPUT);
  setupPin(LEDs_PORT, LEDs_DDR, LED_CS4, OUTPUT);

  init_usart();
}

void toggleLedCS1(){
  *LEDs_PORT ^= (1 << LED_CS1);
}

void toggleLedCS2(){
  *LEDs_PORT ^= (1 << LED_CS2);
}

void toggleLedCS3(){
  *LEDs_PORT ^= (1 << LED_CS3);
}

void toggleLedCS4(){
  *LEDs_PORT ^= (1 << LED_CS4);
}

Et son .h :

#ifndef HARDWARE_H
#define HARDWARE_H

#include <stdint.h>

// ------------------ Enum ------------------ //
typedef enum {
  INPUT,
  INPUT_PULL_UP,
  OUTPUT,
} pinmode;

// ------------------ LEDs ------------------ //
#define LEDs_PORT &PORTD
#define LEDs_DDR &DDRD
#define LEDs_PIN PIND

#define LED_CS1 3
#define LED_CS2 2
#define LED_CS3 1
#define LED_CS4 0


// ------------------ Prototypes ------------------ //
void setupClock();

void setupPin(volatile uint8_t* PORTx, volatile uint8_t* DDRx, uint8_t pin, pinmode mode);
int readPin(volatile uint8_t* PINx, uint8_t pin);
void setupHardware();

void toggleLedCS1();
void toggleLedCS2();
void toggleLedCS3();
void toggleLedCS4();

void taskToggleCS1();
void taskToggleCS2();
void taskToggleCS3();
void taskToggleCS4();

#endif

Dans la librairie hardware.c nous avons un ensemble de fonctions liées à notre matériel pour son initialisation et son contrôle ou sa lecture (de pin).

USART

Dans la librarie USART, on définit les fonctions basiques pour initialiser, envoyer et recevoir depuis l'usart. L'usart nous servira par la suite pour certaines tâches.

#include "usart.h"
#include <avr/io.h>
#include <stdint.h>
#include <util/delay.h>

void init_usart() {
  // Serial Initialization
  /*Set baud rate 9600 */
  UBRR0H = (unsigned char)(UBRR_VALUE >> 8);
  UBRR0L = (unsigned char)UBRR_VALUE;

  /* Enable receiver and transmitter */
  UCSR0B = (1 << RXEN0) | (1 << TXEN0);

  /* Frame format: 8data, No parity, 1stop bit */
  UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);
}

void usart_send(unsigned char data) {
  while (!(UCSR0A & (1 << UDRE0)));
  UDR0 = data;
}

unsigned char usart_receive() {
  while (!(UCSR0A & (1 << RXC0)));
  return UDR0;
}

Et usart.h :

#ifndef USART_H
#define USART_H

// Baud rate variable que l'on peut modifier
#define BAUD_RATE 9600

#define F_CPU 16000000UL
#define UBRR_VALUE ((F_CPU / 16 / BAUD_RATE) - 1)

void init_usart();
void usart_send(unsigned char data);
unsigned char usart_receive();

#endif
TASK

Task.c :

#include "../HARDWARE/hardware.h"
#include "../ORDONNANCEUR/ordonnanceur.h"
#include "../USART/usart.h"

#include "task.h"

process task[] = {
    // {taskSendSerialA, 0x0780, AWAKE, 0},
    {taskCS1, 0x0730, AWAKE, 0},
    {taskSendSerialB, 0x06E0, AWAKE, 0},
    // {taskCS2, 0x0690, AWAKE, 0},
    // {taskCS3, 0x06E0, AWAKE, 0},
    // {taskCS4, 0x0620, AWAKE, 0},
};

const uint8_t nbTasks = sizeof(task) / sizeof(task[0]);

void init_tasks() {
  for (int i = 0; i < nbTasks; i++) {
    init_pile(i);
  }
}

void taskCS1() {
  while (1) {
    toggleLedCS1();
    _delay_ms(1000);
  }
}

void taskCS2() {
  while (1) {
    toggleLedCS2();
    _delay_ms(500);
  }
}

void taskCS3() {
  while (1) {
    toggleLedCS3();
    _delay_ms(500);
  }
}

void taskCS4() {
  while (1) {
    toggleLedCS4();
    _delay_ms(250);
  }
}

void taskSendSerialA() {
  while (1) {
    usart_send('A');
    usart_send('\n');
    usart_send('\r');
    _delay_ms(500);
  }
}

void taskSendSerialB() {
  while (1) {
    usart_send('B');
    usart_send('\n');
    usart_send('\r');
    _delay_ms(250);
  }
}

Task.h:

#ifndef TASK_H
#define TASK_H

typedef enum {
  AWAKE,
  SLEEP,
} state_task;

typedef struct {
  void (*taskAddress)(void); // fonction de la tache
  uint16_t stackPointer;     // pointeur de pile
  state_task state;          // AWAKE ou SLEEP
  uint16_t sleepTime;        // temps restant en ms
} process;

extern process task[];
extern const uint8_t nbTasks;

void init_tasks();
void task_led(void (*toggleFunc)(void), uint16_t ms);

void taskCS1();
void taskCS2();
void taskCS3();
void taskCS4();
void taskSendSerialA();
void taskSendSerialB();

#endif

Une tâche possède :

  • un pointeur de fonction avec le nom de la tâche
  • un pointeur pour l'adresse mémoire afin de savoir où est située la tâche dans la pile
  • l'état de la tâche : actif ou non (endormi)
  • le temps pour lequel la tâche reste endormie

On définit un tableau de tâches ainsi :

process task[] = {
    // {taskSendSerialA, 0x0780, AWAKE, 0},
    {taskCS1, 0x0730, AWAKE, 0},
    // {taskSendSerialB, 0x06E0 , AWAKE, 0},
    // {taskCS2, 0x0690, AWAKE, 0},
    // {taskCS3, 0x06E0, AWAKE, 0},
    // {taskCS4, 0x0620, AWAKE, 0},
};

const uint8_t nbTasks = sizeof(task)/sizeof(task[0]);

Le nombre de tâche est calculé automatique pour ne pas s'embêter à le faire à chaque fois que l'on modifie de le tableau de tâche.

Si certaines tâches sont commentés c'est parceque sur cette 1ere version d'ordonnanceur, nous avons fais au plus simple sans contrôle de tache endormi, nous ne pourrons donc pas avoir un résultat propre avec plusieurs tâches simultanément.

ORDONNANCEUR

ordonnanceur.c :

#include "ordonnanceur.h"
#include <avr/interrupt.h>

#include "../HARDWARE/hardware.h"

int currentTask = 0;
uint16_t tick_ms = 0;

// ------------------ TIMER ------------------ //
void init_timer1(int diviseur, long periode_ms) {
  tick_ms = periode_ms;

  TCCR1A = 0;
  TCCR1B = (1 << WGM12); // CTC mode

  switch (diviseur) {
  case 64:
    TCCR1B |= (1 << CS11) | (1 << CS10);
    break;
  case 256:
    TCCR1B |= (1 << CS12);
    break;
  }

  OCR1A = (F_CPU / diviseur) * periode_ms / 1000;
  TCNT1 = 0;
  TIMSK1 = (1 << OCIE1A);
}

// ------------------ PILE ------------------ //
void init_pile(int n) {
  uint16_t savedSP = SP;
  uint16_t addr = (uint16_t)task[n].taskAddress;

  SP = task[n].stackPointer;

  // PC (low puis high)
  asm volatile("push %A0" ::"r"(addr));
  asm volatile("push %B0" ::"r"(addr));

  // r0-r31
  for (int i = 0; i < 32; i++)
    asm volatile("push __zero_reg__");

  // SREG = I=1
  uint8_t s = 0x80;
  asm volatile("push %0" ::"r"(s));

  task[n].stackPointer = SP;
  SP = savedSP;
}

// ------------------ SCHEDULER ------------------ //
void scheduler(void) {
  currentTask = (currentTask + 1) % nbTasks;
}

// ------------------ ISR TIMER1 ------------------ //
ISR(TIMER1_COMPA_vect, ISR_NAKED) {

  /* Sauvegarde du contexte de la tâche interrompue */
  SAVE_REGISTERS();
  task[currentTask].stackPointer = SP;

  /* Appel à l'ordonnanceur qui choisi la prochaine tache */
  scheduler();

  /* Récupération du contexte de la tâche ré-activée */
  SP = task[currentTask].stackPointer;
  RESTORE_REGISTERS();

  asm volatile("reti");
}

Nous avons ici un ordonnanceur préemptif : une fois que le minuteur a atteint son nombre de "ticks", il appelle l'ISR : Interrupt Service Routine qui va :

  • Sauvegarder les registres grâce à la fonction assembleur SAVE_REGISTERS définie dans ordonnanceur.h, qui permet de sauvegarder les registres de la tâche interrompue
  • Appeller l'ordonnanceur qui va faire la bascule des tâches.
  • Restorer le contexte, et tous les registres de la tâche que l'on va exécuter.
ISR(TIMER1_COMPA_vect, ISR_NAKED){
    
    /* Sauvegarde du contexte de la tâche interrompue */
    SAVE_REGISTERS();
    task[currentTask].stackPointer = SP;

    /* Appel à l'ordonnanceur qui choisi la prochaine tache */
    scheduler();

    /* Récupération du contexte de la tâche ré-activée */
    SP = task[currentTask].stackPointer;
    RESTORE_REGISTERS();

    asm volatile("reti");
}

L'ordonnanceur est ici round-robin, il exécute donc les tâches les unes après les autres sans priorité sous sa forme la plus minimaliste.

MAIN

Dans le fichier main.c, voici comment les librairies vues ensemble précedemment sont appelées :

#include <avr/interrupt.h>
#include <util/delay.h>

#include "./lib/HARDWARE/hardware.h"
#include "./lib/ORDONNANCEUR/ordonnanceur.h"

#include "./lib/TASK/task.h"
#include "./lib/USART/usart.h"

int main(void) {
  cli(); // désactiver interruptions

  setupHardware();

  // initialisation des piles
  init_tasks();

  // TIMER1 config à 20 ms
  init_timer1(64, 20);

  // charger la pile de la premi??re t??che
  SP = task[0].stackPointer;
  RESTORE_REGISTERS();

  asm volatile("reti");

  return 0;
}

Carte mère

La deuxième carte à réaliser est la carte mère avec une spécificité cependant, à savoir une puce STM32F410R8T6 en tant que microcontrôleur.

Remarque : M. Redon a également fait une carte mère mais basée sur un ATSAMD21G8A-A sur notre Git.

Hardware

Microprocesseur

On utilise la puce STM32F410R8T6 basée sur un Cortex-M4.

Signification du nom de la puce

Signification (cf p 134 de la short datasheet) :

Arm based 32-bit microcontroller

  • F = General-purpose
  • R = 64 pins
  • 8 = 64 Kbytes of Flash memory
  • T = package LQFP
  • 6 = Industrial temperature range, - 40 to 85 °C
Datasheets

Datasheet de la puce STM32F410R8T6 :

STM32_datasheet
STM32_datasheet

On peut également retrouver des pages supplémentaires afin de bien dimensionner notre quartz et les capacités aux alentours. Document AN2867- Guidelines for oscillator design on STM8AF/AL/S and STM32 MCUs/MPUs à retrouver sur le lien suivant : https://www.st.com/en/microcontrollers-microprocessors/stm32f410/documentation.html Toutes les datasheets ayant servi pour la creation du Kicad de la carte se trouve dans le dossier Datasheet.

Schématique

Notre schématique est composée de quatres sous feuilles, respectivement pour l'alimentation, le microcontrôleur, la carte mémoire et les cartes filles.

Alimentation

L'alimentation se fait via du 5V et est ensuite directement convertie en 3,3V par le biais du régulateur afin d'alimenter le microcontrôleur. C'est pourquoi les bus de données USB ne sont pas utilisées car l'USB servira ici uniquement à l'alimentation et pas à la transmission de données. On ajouté également des mounting holes pour fixer la carte.

Carte mémoire

La carte mémoire ou carte fille SD est sensiblement la même que celle pour le shield. On a juste rajouté une capacité de découplage car la carte SD va recevoir et envoyer beaucoup de données rapidement.

Microcontrôleur

Le microcontrôleur est composé de beaucoup de broches dédiées à l'alimentation, aux horloges, aux boots, à la communication, aux cartes filles, aux switchs, aux leds et au JTAG (voir sections suivantes).

Alimentation

Les broches VDD servent à l'alimentation numérique et VDDA à l'alimentation analogique, ici séparée pour filtrer de manière plus précise car plus sensible que le numérique. En effet, pour filtrer les hautes fréquences en numérique, les capacités de découplage suffisent alors qu'en analogique le signal d'entrée nécessite une gestion plus précise avec une ferrite.

Horloges

On a ici deux horloges :

  • Première horloge : on peut soit choisir l'oscillateur RC de 16 MHz ou une horloge externe comprise entre 4 et 26 MHz (p18/142)
  • Deuxième horloge : horloge pour le temps réel de 32kHZ (donc pas une application qu'on vise) (p22/142)
Boot et configuration

Les boot 0 et 1 permettent de choisir le bloc mémoire  :

- la flash (boot0 à 0) : mémoire non volatile pour le code principal. Adresse : 0x0800 0000 - 0x0801 FFFF d'après le memory mapping (p43/142).

- la ROM - Read Only Memory (boot0 à 1 et boot1 à 0) : mémoire non volatile que l'on change rarement sauf si besoin de changer mode communication par exemple (passage en spi, uart ...). Adresse : 0x1FFF 0000 - 0x1FFF 77FF.

- la SRAM - Static Random Access Memory (boot0 à 1 et boot1 à 1) : mémoire volatile pour le débogage. Adresse : 0x2000 0000 - 0x2000 7FFF.

On a aussi le pin NRST (Not Reset car actif à l'état bas) pour réinitialiser le microcontrôleur.

Communication

On a prévu différents types de communications selon les utilisations : SPI pour les cartes filles mais aussi UART et I2C amélioré si besoin pour une potentielle carte FPGA.

Cartes filles

On a prévu de la place pour 5 cartes filles, sans compter la carte mémoire et la carte FPGA potentielle.

Leds

3 leds supplémentaires ont étés ajoutées pour différents tests, utile pour tester en premier lieu le microcontrôleur puis l'ordonnanceur.

JTAG et SWD

Le bloc JTAG sert pour la programmation de la carte :

  • SWCLK : comme TCK
  • SWDIO : comme TMS
  • SWO : comme TDO
Cartes filles

Notre carte mère peut acceuillir 5 cartes filles communicantes en SPI parmi lesquelles :

  • carte clavier
  • carte écran
  • carte réseau
  • carte son
  • une autre carte

Et en plus de cela, on a aussi la carte "fille" pour la gestion de la mémoire = le boîtier SD (en SPI également) ainsi que la carte fille FPGA ou d'autre cartes qui peuvent communiquer en UART ou I2C amélioré (car SMBA).

Mere schematique
Mere schematique

Vue 3D

Carte mere 3D
Carte mere 3D
Carte mere 3D backside
Carte mere 3D backside

Brasure

INSERER PHOTO

Software

NUCLEO-F410RB

En attendant de recevoir notre carte mère et afin de prendre en main la programmation quelque peu spécifique des arm, on s'entraîne sur la carte NUCLEO-F410RB qui possède le même microcontrôleur STM32F410R8T6 que celui de notre carte mère.

Spécifications

La carte NUCLEO-F410RB est "séparée" en deux parties :

  • la partie haute de la carte : c'est le programmateur spécifique à STM32 nommé ST-LINK qui permet de programmer :
  • la partie basse de la carte : qui est elle dédiée à l'utilisateur.
STM32CubeIDE
Test led

Pour commencer à comprendre, on utilise dans un premier temps l'IDE STM32CubeIDE dont l'on se passera ensuite afin d'être proche du matériel et de ne pas passer par plein de librairies.

Dans notre fichier nucleo.ioc, on a toutes les spécifications de notre carte dont le pinout.

Premier programme test : on fait clignoter la led LD2. On voit sur le fichier nucleo.ioc que LD2 est sur le pin PA5 du microcontrôleur.

En allant dans l'arborescence à gauche, on va dans Core -> Src -> main.c.

On ouvre le main et dans la boucle while, on va faire clignoter notre led D2 grâce à la librairie HAL (Hardware Abstraction Layer) dont l'on se passera par la suite (c'est juste pour les premiers tests).

 while (1)
  {
	  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); //change d'état
	  HAL_Delay(500); //attend 500ms
  }

C'est un peu l'équivalent arduino, on code avec l'abstraction matérielle. Nous allons par la suite procéder au niveau bare metal.

Bare metal

Ce chapitre présente la programmation Bare Metal sur microcontrôleurs ARM, en utilisant un STM32 NUCLEO-F410RB comme exemple. Nous aborderons le startup, le linker, les exemples de clignotement LED, l'utilisation de CMSIS, ainsi que la gestion de périphériques comme le SPI, l'UART, etc...

Startup (.s)

Le Startup file est crucial en Bare Metal. Une erreur dans ce fichier peut empêcher le microcontrôleur de démarrer ou provoquer des crashs. Rôles principaux :

- Définir l'environnement nécessaire à l'exécution de main().

- S'exécuter avant main() et lancer ensuite main().

- Être adapté à la target (processeur) utilisée.

- Placer correctement la table des vecteurs comme exigé par les ARM Cortex-M.

- Initialiser la pile correctement.

- Initialiser les sections .data et .bss dans la SRAM.

Linker (.ld)

Le Linker Script détermine comment les sections du code sont placées en mémoire. Fonctionnalités :

  • Définir les adresses absolues des sections.
  • Définir les zones mémoire, leurs tailles et adresses.
  • Fournir les instructions au linker GNU via l'option -T.
  • L'extension de fichier est .ld.

Le code linker sert à guider le compilateur pour assembler toutes les sections d’un programme en un fichier binaire unique. Sur un microcontrôleur comme le STM32F410RB, il faut indiquer où chaque partie du code et des données doit être placée dans la mémoire du MCU :

- Où commence le code executable, son point d'entrée ;

- Définit chaque region de la memoire qui existe sur le microcontrôleur et leur taille ;

- Où placer les sections differentes du code en memoire (exemple : interruption, etc...). Dans l'idée on peut le faire à la main afin de mieux assimiler le linker puis récuperer celui générer automatiquement par le logiciel STM32IDE (également disponible sur github) afin de ne pas avoir a y retoucher a chaque fois et ne pas briquer accidentellement notre puce. Pour realiser un linker on doit preciser la SRAM et sa flash. Cette partie décrit les zones mémoire physiques du STM32F410RB. Il faut se fier au plan memoire de la datasheet p.40/763 Sur celui-ci nous constatons que SRAM est à 32kB et commence à 0x2000 0000 et que la fash commence à 0x0800 0000 et sa taille est de 128kB (donné à la figure 1 page 37/763).

On peut alors placer dans le linker :

MEMORY
{
  FLASH (rx): ORIGIN = 0x08000000, LENGTH = 128K
  SRAM (rwx): ORIGIN = 0x20000000, LENGTH = 32K
}

(rx) → read et execute, donc on peut lire et exécuter du code dedans.

(rwx) → read, write, execute (en pratique, on n’exécute pas depuis la RAM, mais certains MCU le permettent). SectionsChaque programme compilé contient plusieurs sections générées par le compilateur. L'ordre à laquelle ils sont cris découle de la convention utilisé dans CMSIS (fichier startup fournis par ARM).


Tous les compilateurs suivent la même idée :

- Sections nécessaires au CPU d’abord (vecteurs, code)

- Sections de données ensuite (initialisées, non initialisées)

- Mémoire dynamique à la fin (heap, stack)

La partie ISR_VECTOR indique les vecteurs d'interruptions :

isr_vector :
{
    KEEP(*(.isr_vector))
} >FLASH

".isr_vector" : C'est la section qui contient le vecteur d'interruptions (ISR - Interrupt Service Routine). Les vecteurs d'interruptions sont des adresses de fonction qui seront appelées lorsque des interruptions spécifiques se produisent.

"KEEP" : Cette directive indique au linker de conserver cette section dans le binaire final, même si elle semble inutilisée par le programme. Cela est crucial pour les vecteurs d'interruptions qui doivent absolument être présents dans le binaire. ">FLASH" : Cela indique que cette section doit être placée en mémoire Flash, qui est généralement de la mémoire non-volatile, utilisée pour stocker le programme. Elle est placée en Flash pour être disponible dès le reset.

La partie TEXT contient le code de notre programme :

 .text :
  {
    . = ALIGN(4);
		
    *(.text)
    *(.rodata)
		
    . = ALIGN(4);
    _etext = .;
  } >FLASH

.text = code exécutable (fonctions, instructions machine).

.rodata = données constantes (const int, chaînes de caractères, etc.).

_etext marque la fin de la zone code, utile pour copier ensuite la section .data au démarrage. ALIGN(4) permet d'aligner l'adresse courante sur une frontière de 4 octets. Cela garantit que le code est correctement aligné en mémoire, ce qui peut être nécessaire pour des performances optimales ou des restrictions matérielles.

La partie DATA contient les variables initialisées :

 .data :
  {
    . = ALIGN(4);
    _sdata = .;
		
    *(.data)

    . = ALIGN(4);
    _edata = .;
  } >SRAM AT> FLASH

.data contient les variables globales initialisées (ex: int x = 5;).

Ces valeurs sont stockées dans la Flash au moment de la programmation, mais copiées dans la RAM lors du démarrage (d’où >SRAM AT>FLASH).

_sdata et _edata servent au code d’initialisation (startup.s) pour savoir quoi copier et combien d’octets.

La partie BSS contient les variables non initialisées :

  .bss :
  {
    . = ALIGN(4);
    _sbss = .;
		
    *(.bss)
		
    . = ALIGN(4);
    _ebss = .;
  } >SRAM

.bss contient les variables globales non initialisées (ex: int counter;).

Ces variables ne sont pas stockées en Flash, car elles ne contiennent pas de valeur initiale.

Au démarrage, le startup code remplit cette zone avec des zéros (memset), d’où “zeroed during startup”.

Et voici le code complet du linker :

MEMORY{
    FLASH (rx): ORIGIN = 0x08000000, LENGTH = 128K
    SRAM (rwx): ORIGIN = 0x20000000, LENGTH = 32K
}

SECTIONS
{
  .isr_vector :
  {
    KEEP(*(.isr_vector))
  } >FLASH

  .text :
  {
    . = ALIGN(4);
		
    *(.text)
    *(.rodata)
		
    . = ALIGN(4);
    _etext = .;
  } >FLASH

  .data :
  {
    . = ALIGN(4);
    _sdata = .;
		
    *(.data)

    . = ALIGN(4);
    _edata = .;
  } >SRAM AT> FLASH

  .bss :
  {
    . = ALIGN(4);
    _sbss = .;
		
    *(.bss)
		
    . = ALIGN(4);
    _ebss = .;
  } >SRAM
}

Il a été tester sur ce main.c :

#include "../01-lib/stm32f410rx.h"
#include <stdint.h>

#define LED_PIN 5

int main(void) {
  // Activer horloge GPIOA
  RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOAEN_Pos);

  // do two dummy reads after enabling the peripheral clock, as per the errata
  volatile uint32_t dummy;
  dummy = RCC->AHB1ENR;
  dummy = RCC->AHB1ENR;

  GPIOA->MODER &= ~(0x3 << (LED_PIN * 2)); // Clear
  GPIOA->MODER |= 0x1 << (LED_PIN * 2);    // Output
  GPIOA->OTYPER &= ~(1 << LED_PIN);        // Push-pull
  GPIOA->PUPDR &= ~(0x3 << (LED_PIN * 2)); // No pull
  while (1) {
    GPIOA->ODR ^= (1 << LED_PIN);
    for (volatile uint32_t i = 0; i < 1000000; i++)
      ;
  }
}

Et le makefile :

# Compilateur et options
CC = arm-none-eabi-gcc

CFLAGS = -mcpu=cortex-m4 -mthumb -Wall -Wextra
CPPFLAGS = -DSTM32F410Rx \
	-I../01-lib/gcc \
	-I../01-lib/Core \
	-I../01-lib

# Linker
LINKER_FILE = faisALaMainLinker.ld
LDFLAGS = -T $(LINKER_FILE)

# Fichiers objets
OBJS = main.o system_stm32f4xx.o startup_stm32f410rx.o

# Cible principale
all: main.elf

main.elf: $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) -lc -lm -lnosys -o $@

# Compilation des fichiers C
main.o: main.c
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

system_stm32f4xx.o: ../00-cmsis-device-f4-master/Source/Templates/system_stm32f4xx.c
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

# Assemblage du startup
startup_stm32f410rx.o: ../01-lib/gcc/startup_stm32f410rx.s
	arm-none-eabi-as -mcpu=cortex-m4 -mthumb $< -o $@

# Converti ELF en BIN
firmware.bin: main.elf
	arm-none-eabi-objcopy -O binary $< $@

# Upload 
flash: firmware.bin
	st-flash write firmware.bin 0x8000000
	
# Nettoyage
clean:
	rm -f *.o *.elf *.bin

size:
	arm-none-eabi-size main.elf
Processus de compilation :

- Le linker assemble ces fichiers et les place correctement en mémoire, générant un fichier .elf.

- Le fichier .elf est converti en .bin et téléversé dans le microcontrôleur via ST-LINK ou OpenOCD.

Schéma chaine compilation


Nous retrouvons notre code principal en .c qui est compiler pour en ressortir des fichiers .o qui seront ensuite traiter dans le bloc Linker qui fera le lien entre le code et les blocs mémoires réelles. A la sortie nous aurons des .elf qui une fois dans le programmer le transformera en .bin et y sera téléversé dans le mircocontrôleur.

CMSIS

C'est à ce lien : https://github.com/STMicroelectronics/cmsis-device-f4 qu'on vient télécharger la bibliothèque CMSIS (Cortex Microcontroller Software Interface Standard).

CMSIS (Cortex Microcontroller Software Interface Standard) simplifie l'accès au matériel, l'équivalent d'un "#include <avr/io.h>" pour ARM.

C'est une bibliothèque fournie par ARM et ST, qui simplifie l'accès au matériel en proposant :

- Des définitions pour tous les registres du MCU

- Les prototypes des fonctions système (comme SystemInit())

- La table des vecteurs d'interruptions (startup code)

Si on veut la bibliothèque contenant plus d'informations et des examples, on peut se fier à ce repertoire : https://github.com/STMicroelectronics/STM32CubeF4/tree/master.

Aide à la compilation avec un simple Blink

Récuperation des fichiers "syscall.c" et "sysmem.c" : https://github.com/STMicroelectronics/STM32CubeF4/tree/master/Projects/STM32F410xx-Nucleo/Templates/STM32CubeIDE/Example/User

Voici le Makefile correspondant :

# Compilateur et options
CC = arm-none-eabi-gcc

CFLAGS = -mcpu=cortex-m4 -mthumb -Wall -Wextra
CPPFLAGS = -DSTM32F410Rx \
	-I../01-lib/gcc \
	-I../01-lib/Core \
	-I../01-lib/User \
	-I../01-lib

# Linker
LINKER_FILE = generatedLinkerIDE.ld
LDFLAGS = -T $(LINKER_FILE)

# Répertoire build et bin
BUILD = build
BIN = bin

# Fichiers source
SRCS := main.c \
       ../01-lib/User/syscalls.c \
       ../01-lib/User/sysmem.c \
       ../01-lib/system_stm32f4xx.c
# Fichier startup asm
ASM_SRCS = ../01-lib/gcc/startup_stm32f410rx.s

# Objets
OBJS = $(patsubst %.c,$(BUILD)/%.o,$(notdir $(SRCS))) \
       $(patsubst %.s,$(BUILD)/%.o,$(notdir $(ASM_SRCS)))


# Cible principale
all: $(BUILD)/main.elf

# Crée le dossier build/ et bin/
$(BUILD):
	mkdir -p $(BUILD)

$(BIN):
	mkdir -p $(BIN)

# Link
$(BUILD)/main.elf: $(BUILD) $(OBJS)
	$(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) -lc -lm -lnosys -o $@

# Compilation des fichiers C
$(BUILD)/%.o: %.c | $(BUILD)
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

$(BUILD)/%.o: ../01-lib/User/%.c | $(BUILD)
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

$(BUILD)/%.o: ../00-cmsis-device-f4-master/Source/Templates/%.c | $(BUILD)
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@

# Assemblage du startup
$(BUILD)/%.o: ../01-lib/gcc/%.s | $(BUILD)
	arm-none-eabi-as -mcpu=cortex-m4 -mthumb $< -o $@

# Converti ELF en BIN
$(BIN)/firmware.bin: $(BUILD)/main.elf | $(BIN)
	arm-none-eabi-objcopy -O binary $< $@

# Upload 
flash: $(BIN)/firmware.bin
	st-flash write $(BIN)/firmware.bin 0x8000000
	
# Nettoyage
clean:
	rm -rf $(BUILD) $(BIN)

size:
	arm-none-eabi-size $(BUILD)/main.elf

et le main.c :

#include "../01-lib/stm32f410rx.h"

#include <stdint.h>

#define LED_PIN 5

int main(void) {
  // Activer horloge GPIOA
  RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOAEN_Pos);

  GPIOA->MODER &= ~(0x3 << (LED_PIN * 2)); // Clear
  GPIOA->MODER |= 0x1 << (LED_PIN * 2);    // Output
  GPIOA->OTYPER &= ~(1 << LED_PIN);        // Push-pull
  GPIOA->PUPDR &= ~(0x3 << (LED_PIN * 2)); // No pull

  while (1) {
    GPIOA->ODR ^= (1 << LED_PIN);
    for (volatile uint32_t i = 0; i < 1000000; i++)
      ;
  }
}

Nous faisons simplement clignoter une led afin de préparer l'environnement de compilation et vérifier que tout fonctionne. Comme ça, si il y a un bug nous pourrons vite isoler cette étape.


Afin de faire clignoter une LED il faut :

1. Activer l'horloge du GPIO correspondant. Pour ce faire, nous modifions le registre RCC->AHB1ENR. (Cf datasheet RM0401 p.119/763)

RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIO<PORT>EN_Pos);

2. Définir la direction du PIN via le registre

GPIO<PORT>->MODER |= 0x1 << (<PIN_NUM> * 2);

"PB2" => PORT = B et PIN_NUM = 2 Ici par exemple le PIN 3 correspond au 6ieme et 7ieme bit de MODER.

GPIO<PORT>->MODER |= 0x1 << (<PIN_NUM> * 2);    // Output

3. Définir le type de sortie (Registre OTYPER)

GPIO<PORT>->OTYPER &= ~(0x1 << (<PIN_NUM>));    // Push pull

4. Définir le type de pull (Registre PUPDR)

GPIO<PORT>->PUPDR &= ~(0x3 << (<PIN_NUM> * 2)); // Forcage 00 : No pull

5. Etape finale : écriture sur le pin (Registre ODR)

GPIO<PORT>->ODR ^= (1 << <PIN_NUM>);

On peut également modifier de façon atomique (donc juste 1 bit pas tous) via le registre BSRR.

    if (GPIO<PORT>->ODR & (1 << <PIN_NUM>))
      GPIO<PORT>->BSRR = (1 << (<PIN_NUM> + 16)); // Ici 16 est l'offset à ajouter afin d'avoir le bon registre pour eteindre
    else
      GPIO<PORT>->BSRR = (1 << <PIN_NUM>);

Lorsque BSRR est lu il est reset juste après et cela permet d'éviter les conflits si une interruption intervient et modifie ODR entre 2 instructions.

Après on peut également toucher à la vitesse du pin (Registre OSPEEDR) si l'on souhaite optimiser la consommation de batterie. Note : Remplacer <PORT> par A ou B ou C ou etc... et remplacer <PIN> par 1,2,etc.. selon ce qu'on veut activer

Acceder a PA2 et PA3 sur la Nucleo

"SB62 and SB63 must be ON, while SB13 and SB14 must be OFF." Extrait de la datasheet UM1724 : Chapitre7.10 p26/91

Il faut dessouder les resistances bridges pour accéder à ces ports. Et ensuite déplacer le jumper sur U5V.

Système de fichier

Afin de débuter le système de fichiers, on connecte notre mémoire (la carte SD) à notre nucleo.

nucleo_sd
nucleo_sd

Test d'initialisation On commence par initialiser notre carte SD grâce à fichier.c et en premier lieu cela ne fonctionnait pas on avait status=1 et erreur=8 (et en conséquent une taille nulle puisqu'il n'arrive pas à initialiser la carte). La carte n'était pas de bonne qualité et ne communiquait pas en SPI mais sans doute avec un autre protocole. Mais par la suite avec la carte SD donnée par M. Redon, on a réussi à la phase d'initialisation. On obtient alors :

  • son type, ici 2 (l'autre type étant 1 pour les micro cartes sd moins performantes)
  • ainsi que sa taille : 3 911 860 secteurs. Un secteur étant de 512 octets, on retrouve bien la taille écrite sur notre carte à savoir 2Gb.
Sd init
Sd init

Suite à cela, on teste l'écriture, qui s'avère opérationelle (status = 1) puis l'écriture, elle aussi fonctionnelle puisque l'on affiche bien les 3 "blocs" voulus.

Sd read test
Sd read test
Sd write test
Sd write test

Commandes pour le système de fichier La prochaine étape est de coder les commandes nécéssaires telles que append, read, remove, rename, copy.

  1. format : efface l'entièreté du système de fichier.
  2. list : liste les noms des fichiers contenus dans le système de fichier, équivalent du "ls" sous linux.
  3. append : créé un fichier si non existant et ajoute du texte si le fichier existe déjà. Commande de la forme append fichier/données. Combine le "touch" (create dans l'énoncé) et l'écriture de données.
  4. read : permet de lire le contenu d'un fichier, équivalent du "cat".
  5. remove : supprime le fichier en paramètre, équivalent du "rm".
  6. rename : renommer un fichier, équivalent du "mv" en moins puissant.
  7. copy : copie un fichier et ses données dans un second fichier, équivalent du "cp".

SPI

Nous allons programmer le SPI sur notre microcontrôleur et tester ce code pour la carte Sparkfun Serial 7-Segments Display. Sur ce git : https://github.com/sparkfun/Serial7SegmentDisplay/wiki/Serial-7-Segment-Display-Datasheet se trouve la datasheet de cette carte que nous allons utiliser pour communiquer en SPI. Dans l'idée, nous allons programmer une bibliothèque suffisament complète afin de pouvoir simplement afficher "9" via une commande dans ce style dans notre main : "spi_sent('G');"

On peut tout d'abord centraliser la logique des initialisations des pins afin d'alléger le main (Cf fichier hardware_setup.c et .h) .

7 segments SparkFun Pinout

Notre carte mère

Afin de vérifier que notre PCB reçu fonctionne, on teste notre carte en faisant clignoter une led.

#include "../00-lib/stm32f410rx.h"

#define LED_PIN 9

int main(void) {
  // Activer horloge GPIOC
  RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOCEN_Pos);

  GPIOC->MODER &= ~(0x3 << (LED_PIN * 2)); // Clear
  GPIOC->MODER |= 0x1 << (LED_PIN * 2);    // Output
  GPIOC->OTYPER &= ~(1 << LED_PIN);        // Push-pull
  GPIOC->PUPDR &= ~(0x3 << (LED_PIN * 2)); // No pull

  while (1) {
    GPIOC->ODR ^= (1 << LED_PIN);
    for (volatile uint32_t i = 0; i < 1000000; i++)
      ;
  }
}

Maintenant que la carte à été testée nous avons fait plusieurs codes intermediaires pour tester chaque primitive séparément. Ici le détail du code final seulement.

Carte

Dans ce dossier nous retrouverons toutes les librairies liées au contrôle des cartes filles correspondantes.

Carte ecran

L'idée est d'avoir une librairie permettant de contrôler une carte écran et d'afficher un compteur sur celui-ci. Notre carte écran est ici l'afficheur 7 segments de Sparkfun.

carteEcran.c :

#include "carteEcran.h"
#include <stdint.h>

// PC11, CS5
#define CS5_GPIO GPIOC
#define CS5_PIN 11

// PB9, RST5
#define RST5_GPIO GPIOB
#define RST5_PIN 9

// PC13, INT5
#define INT5_GPIO GPIOC
#define INT5_PIN 13

uint8_t isChange = 0b1111;

int unite = 0;
int decimal = 0;
int centaine = 0;
int mil = 0;

void _ecran_init() {
  setupPin(CS5_GPIO, CS5_PIN, OUTPUT);
  setupPin(RST5_GPIO, RST5_PIN, OUTPUT);
  // setupPin(INT5_GPIO, INT5_PIN, OUTPUT);

  // Ces pins s'active à l'etat bas
  onPin(CS5_GPIO, CS5_PIN); // CS OFF

  offPin(RST5_GPIO, RST5_PIN); // RST ON
  onPin(RST5_GPIO, RST5_PIN);  // RST OFF

  // Non utilisé ici
  //   onPin(INT5_GPIO, INT5_PIN); // S'active à l'etat bas ?
}

void ecran_spi_write(uint8_t data) {
  spi_write(data, CS5_GPIO, CS5_PIN);
}

void ecran_brightness(uint8_t intensite) {
  ecran_spi_write(0x7A);      // Commande "Brightness"
  ecran_spi_write(intensite); // Sécurité passive 2^8-1 = 255 qui est le maximum
}

void ecran_clear() {
  ecran_spi_write(0x76); // Commande "Clear"
}

void ecran_select_digit(uint8_t digit) {
  // Sécurité maximum digit
  if (digit >= 3)
    digit = 3;

  ecran_spi_write(0x79);  // Cursor command
  ecran_spi_write(digit); // 0 à 3
}

void ecran_compteur() {

  ecran_select_digit(3);
  ecran_spi_write(unite); // Data

  if (isChange & 0b0010) {
    isChange &= ~0b0010;
    ecran_select_digit(2);
    ecran_spi_write(decimal); // Data
  }

  if (isChange & 0b0100) {
    isChange &= ~0b0100;
    ecran_select_digit(1);
    ecran_spi_write(centaine); // Data
  }

  if (isChange & 0b1000) {
    isChange &= ~0b1000;
    ecran_select_digit(0);
    ecran_spi_write(mil); // Data
  }

  if (unite < 9) {
    unite++;
  } else {
    isChange |= 0b0010;
    unite = 0;
    if (decimal < 9)
      decimal++;
    else {
      decimal = 0;
      isChange |= 0b0100;

      if (centaine < 9) {
        centaine++;
      } else {
        centaine = 0;
        isChange |= 0b1000;

        if (mil < 9) {
          mil++;
        } else {
          mil = 0;
        }
      }
    }
  }
}

Et son .h :

#pragma once

#include "../../SPI/spi.h"
#include "../../GPIO/gpio.h"

// Fonction inter librairie
void _ecran_init();

// Fonctions pour l'utilisateur
void ecran_spi_write(uint8_t data);
void ecran_brightness(uint8_t intensite);
void ecran_select_digit(uint8_t digit);

// Fonction exemple pour utilisateur
void ecran_compteur();

Les commandes de l'écran ont été trouvé sur leur site : https://learn.sparkfun.com/tutorials/using-the-serial-7-segment-display/all

Il faut noter que la vitesse du SPI peut influencer la précision du SPI et donc faire échouer certains messages si la vitesse dépasse 9600 bauds rate.

Carte clavier

Cette partie est assez complexe et sera implémentée plus tard.

Commande OS

Cette librairie permet d'implémenter les commandes liés à notre OS tel que "version", "echo, et "devices".

cmd_os.c:

#include "cmd_os.h"

#include "../DEVICES/devices.h"
#include "../Substitute/printf.h"
#include "../TASK/task.h"

#include <string.h>

#define MAX_ARG_ECHO 20
#define CMD_SIZE 128

#define CHAR_CODE_ESC 0x1B
#define CHAR_CODE_ENTER '\r'
#define CHAR_CODE_BACKSPACE1 '\b'
#define CHAR_CODE_BACKSPACE2 0x7F

typedef enum {
  KEY_NONE,
  KEY_CHAR,
  KEY_ENTER,
  KEY_BACKSPACE,
  KEY_ARROW_UP,
  KEY_ARROW_DOWN,
  KEY_ARROW_LEFT,
  KEY_ARROW_RIGHT
} KeyType;

typedef enum {
  NORMAL_CHAR = 0,
  ESC_CHAR = 1,
  ARROW_CHAR = 2,
} _stateKey;

typedef struct {
  KeyType type;
  char ch;
} KeyEvent;

char current_cmd[CMD_SIZE + 1] = {'\0'};
static uint8_t cmd_len = 0;
// static uint8_t cursor_pos = 0;

_stateKey esc_state = NORMAL_CHAR;
KeyEvent ev = {KEY_NONE, 0};

KeyEvent _decode_key(char c) {
  switch (esc_state) {
  case NORMAL_CHAR:
    ev.type = KEY_NONE;
    ev.ch = 0;
    if (c == CHAR_CODE_ESC) { // ESC
      esc_state = ESC_CHAR;
      return ev; // attente suite
    }

    if (c == CHAR_CODE_ENTER) {
      ev.type = KEY_ENTER;
    } else if (c == CHAR_CODE_BACKSPACE1 || c == CHAR_CODE_BACKSPACE2) {
      ev.type = KEY_BACKSPACE;
    } else {
      ev.type = KEY_CHAR;
      ev.ch = c;
    }
    return ev;

  case ESC_CHAR:
    if (c == '[' || c == 'O') {
      esc_state = ARROW_CHAR;
    } else {
      esc_state = NORMAL_CHAR; // ESC seul = abandon
    }
    return ev;

  case ARROW_CHAR:
    switch (c) {
    case 'A':
      ev.type = KEY_ARROW_UP;
      break;
    case 'B':
      ev.type = KEY_ARROW_DOWN;
      break;
    case 'C':
      ev.type = KEY_ARROW_RIGHT;
      break;
    case 'D':
      ev.type = KEY_ARROW_LEFT;
      break;
    default:
      // ignore les autres codes
      break;
    }
    esc_state = NORMAL_CHAR;
    return ev;
  }

  ev.type = KEY_NONE;
  ev.ch = 0;
  esc_state = NORMAL_CHAR;
  return ev;
}

void _cmdOS_inconnu() {
  PRINT_STRING("\r\nCommande inconnue : ");
  PRINT_STRING(current_cmd);
  PRINT_STRING("\r\n");
}

void _cmdOS_version() {
  PRINT_STRING("\r\nOS : version 0.0.2\n\r");
}

void _cmdOS_echo(char *input) {
  char *argv[MAX_ARG_ECHO];
  int argc = 0;

  // Découpe la ligne en mots
  char *token = strtok(input, " ");
  while (token != NULL && argc < MAX_ARG_ECHO) {
    argv[argc++] = token;
    token = strtok(NULL, " ");
  }

  if (argc > 0) {
    PRINT_STRING("\r");
    for (int i = 1; i < argc; i++) {
      PRINT_STRING(argv[i]);
      if (i + 1 < argc)
        PRINT_STRING(" ");
    }
    PRINT_STRING("\r\n\n");
  }
}

void _cmdOS_devices(void) {
  scan_devices();
  print_devices();
}

void _execute_command(char *cmd) {
  if (strcmp(cmd, "version") == 0)
    _cmdOS_version();
  else if (strcmp(cmd, "devices") == 0)
    _cmdOS_devices();
  else if (strncmp(cmd, "echo", strlen("echo")) == 0)
    _cmdOS_echo(cmd);
  else if (cmd_len > 0)
    _cmdOS_inconnu();
}

void _init_os() {
  PRINT_STRING("Initialisation du Pico ordinateur ");

  for (int i = 0; i < 6; i++) {
    task_delay(100);
    PRINT_STRING(".");
  }

  PRINT_STRING("\n\rPicoOrdi>");
}

void cmd_os(void) {
  _init_os();

  while (1) {
    while (WAITCHAR())
      ;

    char c = GETCHAR();
    KeyEvent key = _decode_key(c);

    switch (key.type) {
    case KEY_CHAR:
      if (cmd_len < CMD_SIZE) {
        current_cmd[cmd_len++] = key.ch;
        current_cmd[cmd_len] = '\0';
        PRINT_CHAR(key.ch);
      }
      break;

    case KEY_BACKSPACE:
      if (cmd_len > 0) {
        cmd_len--;
        current_cmd[cmd_len] = '\0';
        PRINT_STRING("\b \b");
      }
      break;

    case KEY_ENTER:
      PRINT_STRING("\r\n");
      _execute_command(current_cmd);
      cmd_len = 0;
      current_cmd[0] = '\0';
      PRINT_STRING("PicoOrdi>");
      break;

    case KEY_ARROW_UP:
      PRINT_STRING("\r\nUP - Gestion historique a implementer\r\nPicoOrdi>");
      break;

    case KEY_ARROW_DOWN:
      PRINT_STRING("\r\nDOWN - Gestion historique a implementer\r\nPicoOrdi>");
      break;

    case KEY_ARROW_LEFT:
      PRINT_STRING("\x1B[D");
      cmd_len--;
      break;

    case KEY_ARROW_RIGHT:
      PRINT_STRING("\x1B[C");
      cmd_len++;
      break;

    default:
      break;
    }
  }
}

cmd_os.h :

#ifndef CMD_OS_H
#define CMD_OS_H

#include "../../../00-lib/stm32f410rx.h"


/*
    Permet de lancer les commandes suivantes : 
    
    version => Affiche la version du système d'exploitation
    echo => Affiche les arguments de la commande
    devices => Scan toutes les cartes et retourne celle connecté
*/

void cmd_os(void); // Affiche la liste des cartes filles connectées

#endif

Pour le moment la gestion du terminal permet de gérer la détection des flèches grâce à une implémentation de la détection de touches avec plusieurs états.

Devices

Cette librairie permet de gérer la détection des cartes filles (généralement après un appel de la fonction "devices").

device.c :

#include "devices.h"
#include "../SPI/spi.h"
#include "../Substitute/printf.h"
#include <stdio.h>

static const GPIO_TypeDef *cs_gpios[MAX_DEVICES] = {
    [CS1] = GPIOC, // PC0
    [CS2] = GPIOA, // PA7
    [CS3] = GPIOC, // PC9
    [CS4] = GPIOA, // PA2
    [CS5] = GPIOC, // PC11
    [CS6] = GPIOA, // PA4
};

static const uint8_t cs_pins[MAX_DEVICES] = {
    [CS1] = 0,  // PC0
    [CS2] = 7,  // PA7
    [CS3] = 9,  // PC9
    [CS4] = 2,  // PA2
    [CS5] = 11, // PC11
    [CS6] = 4,  // PA4
};

Device devices[MAX_DEVICES];

static const char *device_names[] = {
    "NOT CONNECTED",
    "UNKNOWN",
    "KEYBOARD",
    "SCREEN",
    "NETWORK",
    "SOUND",
    "SD"};

static const char *cs_names[] = {
    "PC0",
    "PA7",
    "PC9",
    "PA2",
    "PC11",
    "PA4"};

static const char *names[] = {
    "Clavier",
    "FPGA",
    "Son",
    "Reseau",
    "Ecran",
    "SD"};

void _devices_init() {
  // Init tableau
  for (int i = 0; i < MAX_DEVICES; i++) {
    devices[i].GPIO_CS = (GPIO_TypeDef *)cs_gpios[i];
    devices[i].PIN_CS = cs_pins[i];
    devices[i].type = UNKNOWN;
  }

  // SD toujours ici
  devices[5].type = SD; // Peut etre tester si connecte ou non

  // Scan du démarrage
  scan_devices();
}

void scan_devices() {
// --------------------------------------------------------------------------------
// A ajouter a la fin
// --------------------------------------------------------------------------------
}

void print_devices() {
  PRINT_STRING("\r\nDevice list:\r\n");
  for (int device = 0; device < MAX_DEVICES; device++) {
    char buffer[15];

    sprintf(buffer, "%d", device);
    PRINT_STRING("N°");
    PRINT_STRING(buffer);
    PRINT_STRING(": ");
    PRINT_STRING(device_names[devices->type]);

    sprintf(buffer, "%d", device + 1);
    PRINT_STRING("\t\tCS");
    PRINT_STRING(buffer);
    PRINT_STRING(": ");
    PRINT_STRING(cs_names[device]);

    if (device < 10)
      PRINT_STRING(" ");

    PRINT_STRING("\t\tName port: ");
    PRINT_STRING(names[device]);
    PRINT_STRING("\r\n");
  }
}

Device get_state_device(DeviceCS CSx) {
  return devices[CSx];
}

device.h :

#ifndef DEVICES_H
#define DEVICES_H

#include "../../../00-lib/stm32f410rx.h"

#define MAX_DEVICES 6

typedef enum {
    NOT_CONNECTED = 0,
    UNKNOWN,
    KEYBOARD,
    SCREEN,
    NETWORK,
    SOUND,
    SD,
} DeviceType;

typedef struct {
    GPIO_TypeDef *GPIO_CS;        // GPIO Device Select
    uint8_t PIN_CS;               // Pin Device Select
    DeviceType type;              // Type de device
} Device;

typedef enum {
    CS1 = 0, // PC0
    CS2 = 1, // PA7
    CS3 = 2, // PC9
    CS4 = 3, // PA2
    CS5 = 4, // PC11
    CS6 = 5, // PA4
} DeviceCS;

void _devices_init(void);
void scan_devices(void);
Device get_state_device(DeviceCS CSx);
void print_devices(void);

#endif
GPIO

Cette librairie permet d'initialiser les pins de notre carte via des fonctions intermediaires pour une meilleure lisibilité. Elle permet aussi de contrôler et lire les pins si besoin.

gpio.c :

#include "./gpio.h"

#include "../CARTE/Ecran/carteEcran.h"
#include "../DEVICES/devices.h"
#include "../SPI/spi.h"
#include "../USART/usart.h"

#define OFFSET_BSRR_OFF 16

#define MODER_CLEAR 0x3
#define MODER_NumberBitParPin 0x2

#define PUPDR_CLEAR 0x3
#define PUPDR_NumberBitParPin 0x2

#define OTYPER_CLEAR 0x1

void setupPin(GPIO_TypeDef *GPIOx, uint8_t PINx, portModeRegister portMode) {
  // ACTIVATION DE L'HORLOGE GPIO SPECIFIQUE
  uint32_t RCC_AHB1ENR_GPIOxEN_Pos;

  if (GPIOx == GPIOA)
    RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOAEN_Pos;
  else if (GPIOx == GPIOB)
    RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOBEN_Pos;
  else if (GPIOx == GPIOC)
    RCC_AHB1ENR_GPIOxEN_Pos = RCC_AHB1ENR_GPIOCEN_Pos;
  else
    return; // GPIO non existant sur ce microcontroleur

  if (!(RCC->AHB1ENR & (1 << RCC_AHB1ENR_GPIOxEN_Pos)))
    RCC->AHB1ENR |= (1 << RCC_AHB1ENR_GPIOxEN_Pos);

  // CLEAR AVANT MODIFICATION
  // On clear après l'activation d'horloge !
  GPIOx->MODER &= ~(MODER_CLEAR << (PINx * MODER_NumberBitParPin));
  GPIOx->PUPDR &= ~(PUPDR_CLEAR << (PINx * PUPDR_NumberBitParPin));
  GPIOx->OTYPER &= ~(OTYPER_CLEAR << PINx); // Push-pull

  // TYPE DE PORT (Input, Output, Alternative, Analogique)
  portPullUpPullDownRegister typePull = NO_PULL;
  uint8_t optionPort = 0x00;

  switch (portMode) {
  case INPUT:
    optionPort = 0x00;
    typePull = PULL_UP;
    break;

  case OUTPUT:
    optionPort = 0x01;
    typePull = NO_PULL;

    if (1)
      GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
    else
      GPIOx->OTYPER |= (1 << PINx); // Open-drain

    break;

  case ALTERNATE_FUNCTION:
    optionPort = 0x02;

    if (1) {
      typePull = NO_PULL;
      GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
      GPIOx->OSPEEDR |= 11 << (PINx * 2);
      // Very high speed => Cf STM32F410 datasheet tableau p95/142
    }

    break;
  case ALTERNATE_FUNCTION_USART:
    optionPort = 0x02;

    typePull = NO_PULL;
    GPIOx->OTYPER &= ~(1 << PINx); // Push-pull
    // GPIOx->OSPEEDR |= 11 << (PINx * 2); // Very high speed => Cf STM32F410 datasheet tableau p95/142
    if (GPIOx == GPIOA && (PINx == 9 || PINx == 10)) {
      // Sélection AF7 pour USART1 (PA9, PA10)
      GPIOx->AFR[1] &= ~(0xF << ((PINx - 8) * 4));
      GPIOx->AFR[1] |= (7 << ((PINx - 8) * 4)); // AF7 = USART1
    }
    break;

  case ANALOG:
    optionPort = 0x03;
    break;

  default:
    optionPort = 0x00;
    typePull = NO_PULL;
    break;
  }

  // Ecriture
  GPIOx->MODER |= optionPort << (PINx * MODER_NumberBitParPin);

  // TYPE DE PULL (No pull, Pull Down, Pull Up)
  uint8_t optionPull = 0x00;

  // Ecriture type pull
  switch (typePull) {
  case NO_PULL:
    optionPull = 0x00;
    break;
  case PULL_UP:
    optionPull = 0x01;
    break;
  case PULL_DOWN:
    optionPull = 0x02;
    break;
  default:
    optionPull = 0x00;
    break;
  }

  // Ecriture
  GPIOx->PUPDR |= optionPull << (PINx * PUPDR_NumberBitParPin);
}

void onPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  GPIOx->BSRR = (1 << PINx); // set
}

void offPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  GPIOx->BSRR = (1 << (PINx + OFFSET_BSRR_OFF)); // reset
}

int readPin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  return (GPIOx->IDR & (1 << PINx));
}

void togglePin(GPIO_TypeDef *GPIOx, uint8_t PINx) {
  // Après sa lecture, le registre BSRR reset à 0 automatiquement
  if (readPin(GPIOx, PINx)) // Lecture pin, si 1 alors eteindre
    offPin(GPIOx, PINx);
  else // Sinon allumer
    onPin(GPIOx, PINx);
}

void setupCarte() {
  _devices_init();

  // 1
  setupPin(GPIOC, 0, OUTPUT); // PC0, CS1
  setupPin(GPIOC, 1, OUTPUT); // PC1, RST1
  setupPin(GPIOC, 2, OUTPUT); // PC2, INT1

  // 2
  setupPin(GPIOA, 7, OUTPUT); // PA7, CS2
  setupPin(GPIOB, 1, OUTPUT); // PB1, RST2
  setupPin(GPIOC, 4, OUTPUT); // PC4, INT2

  // 3
  setupPin(GPIOC, 9, OUTPUT); // PC9, CS3
  setupPin(GPIOB, 6, OUTPUT); // PB6, RST3
  setupPin(GPIOB, 5, OUTPUT); // PB5, INT3

  // 4
  setupPin(GPIOA, 2, OUTPUT); // PA2, CS4
  setupPin(GPIOA, 1, OUTPUT); // PA1, RST4
  setupPin(GPIOA, 0, OUTPUT); // PA0, INT4

  // 6
  setupPin(GPIOA, 4, OUTPUT); // PA4, CS6

  // Ecran
  _ecran_init();

  // FPGA
  setupPin(GPIOB, 0, OUTPUT); // PB0, CS_FPGA

  // LEDs
  setupPin(GPIOB, 8, OUTPUT); // PB8, LED1
  setupPin(GPIOA, 6, OUTPUT); // PA6, LED2
  setupPin(GPIOB, 7, OUTPUT); // PB7, LED3

  // BTNs
  setupPin(GPIOC, 12, INPUT); // PC12, SW_1
  setupPin(GPIOB, 11, INPUT); // PB11, SW_2
  setupPin(GPIOC, 10, INPUT); // PC10, SW_3

  // SPI
  spiInit();

  // On eteint tous les RST
  onPin(GPIOC, 1); // PC1, RST1

  usart_init(115200);
}

gpio.h :

#ifndef GPIO_H
#define GPIO_H

#include "../../../00-lib/stm32f410rx.h"

typedef enum {
  INPUT,
  OUTPUT,
  ALTERNATE_FUNCTION,
  ALTERNATE_FUNCTION_USART,
  ANALOG,
} portModeRegister;

typedef enum {
  NO_PULL,
  PULL_UP,
  PULL_DOWN,
} portPullUpPullDownRegister;

#define CLOCK_MHZ 16 // HSI = 16MHz Cf STM32F410 datasheet p82/142

void setupPin(GPIO_TypeDef *GPIOx, uint8_t PINx, portModeRegister portMode);
int readPin(GPIO_TypeDef *GPIOx, uint8_t PINx);
void offPin(GPIO_TypeDef *GPIOx, uint8_t PINx);
void onPin(GPIO_TypeDef *GPIOx, uint8_t PINx);
void togglePin(GPIO_TypeDef *GPIOx, uint8_t PINx);

void setupCarte();

#endif
Ordonnanceur

Cette librairie ainsi que TASK sont les plus importantes car elles étaient les plus difficiles à implémenter. Aucun étudiant n'avait déjà travailler sur un ordonnanceur sur une architecture ARM et peu d'information sont présentes sur internet sur ce sujet. Les professeurs m'ont confiés ce sujet afin que je puisse aider à l'évolution du module pico ordinateur avec peut être plus de microcontrôleur sur ARM pour les prochaines années.

C'était donc un défi très intéressant de réussir à coder les primitives d'un ordonnanceur sur ARM.

ordonnanceur.c :

#include "ordonnanceur.h"

uint32_t g_tick_count = 0;
uint32_t INCREMENT_TIMER = 0;

extern TCB_t *current_task_ptr;

/* ------------------ Fonctions interne ------------------ */
uint32_t _get_psp_addr(void) {
  if (!current_task_ptr)
    return 0;
  return (uint32_t)current_task_ptr->psp_addr;
}

void _save_psp_addr(uint32_t addr) {
  if (!current_task_ptr)
    return;
  current_task_ptr->psp_addr = (uint32_t *)addr;
}

void _setupTimer5(uint32_t ms) {
  INCREMENT_TIMER = ms;
  // Choix TIM5 car gestion d'un timer simple

  // ACTIVATION DE L'HORLOGE TIM5 SPECIFIQUE
  RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;

  // Prescaler Register
  TIM5->PSC = (CLOCK_MHZ - 1); // Diviser par Clock pour avoir 1 MHz

  // ARR : Auto Reload Register
  TIM5->ARR = (ms * 1000) - 1; // 1/1MHz * 1000 devient des millisecondes
  // valeur à laquelle le timer reset et déclenche une interruption Cf p341

  // Counter Register
  TIM5->CNT = 0; // On commencer a compter à 0

  // DMA/Interrupt enable register
  TIM5->DIER |= TIM_DIER_UIE; //  Update interrupt enable

  // Activer TIM5 en mode compteur
  TIM5->CR1 |= TIM_CR1_CEN;

  NVIC_SetPriority(USART1_IRQn, 0x01);
  NVIC_SetPriority(TIM5_IRQn, 0x10);
  NVIC_SetPriority(PendSV_IRQn, 0x3);

  NVIC_EnableIRQ(TIM5_IRQn);
  NVIC_EnableIRQ(PendSV_IRQn);
}

void TIM5_IRQHandler(void) {
  // Clear flag
  TIM5->SR &= ~TIM_SR_UIF;

  g_tick_count += INCREMENT_TIMER;
  _unblock_tasks();

  // Déclenche PendSV (switch context)
  SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}

// Bascule MSP à PSP
__attribute__((naked)) void launch_os(void) {
  // Initialisation PSP avec la pile de la tâche courante
  __asm volatile("PUSH {LR}");        // Sauvegarde LR (adresse du main() )
  __asm volatile("BL _get_psp_addr"); // Appelle _get_psp_addr(), retourne le PSP de la tâche courante dans r0
  __asm volatile("MSR PSP,R0");       // Met à jour le PSP avec la nouvelle valeur
  __asm volatile("POP {LR}");         // Restaure LR sauvegardé avant les appels BL

  // Bascule du Stack Pointer actif de MSP vers PSP
  __asm volatile("MOV R0,#0X02");   // Selection du PSP en Thread mode (CONTROL.SPSEL = 1)
  __asm volatile("MSR CONTROL,R0"); // Mise à jour du registre CONTROL
  __asm volatile("CPSIE I");        // Active les IRQ
  __asm volatile("BX LR");          // Retour à l'appelant en utilisant désormais le PSP
}

// Quand PendSV est executé, le systeme enregistre le contexte de la tâche courante automatiquement.
// Ici, il est utilisé en naked donc on enregistre à la mano.
__attribute__((naked)) void PendSV_Handler(void) {
  SAVE_REGISTERS();

  // PC saute à l'adresse
  __asm volatile("BL _scheduler");

  RESTORE_REGISTERS()
}

void init_os(void) {
  __disable_irq();
  _setupTimer5(1);
  _init_tasks();
}

ordonnanceur.h :

#ifndef ORDONNANCEUR_H
#define ORDONNANCEUR_H

#include "../GPIO/gpio.h"
#include "../TASK/task.h"


#define SAVE_REGISTERS() \
    __asm volatile("MRS r0, PSP                 @ r0 = PSP courant\n\t"  \
                   "STMDB r0!, {r4-r11}         @ Sauvegarde registre R4 à R11 sur la pile PSP\n\t"  \
                   "PUSH {LR}                   @ Sauvegarde LR sur la pile MSP avant BL\n\t" \
                   "BL _save_psp_addr            @ Appelle save_psp_addr(r0) pour mémoriser le PSP\n\t");

#define RESTORE_REGISTERS() \
    __asm volatile("BL _get_psp_addr             @ Appelle get_psp_addr(), retourne le PSP de la tâche suivante dans r0\n\t"\
        "LDMIA r0!, {r4-r11}                    @ Restaure R4 à R11 depuis la pile de la nouvelle tâche\n\t"\
        "MSR PSP, r0                            @ Met à jour le PSP avec la nouvelle valeur\n\t"\
        "POP {LR}                               @ Restaure LR sauvegardé avant les appels BL\n\t"\
        "BX LR                                  @ Retour d'exception : sortie de PendSV vers la tâche sélectionnée\n\t");

/* ------------------ Function Prototypes ------------------ */
void init_os(void);
void launch_os(void);

#endif
Tâche

Contrairement à la gestion des tâches sur la carte shield, ici nous avons une gestion dynamique avec une liste chaînée (vu au semestre 6 avec M. FORGET).

task.c :

#include "task.h"

#include <stddef.h>
#include <stdlib.h>

#define DUMMY_XPSR 0x01000000 // xPSR (32bits), 24eme bit indique thumb mode sinon crash

/* ------------------ Variables globales ------------------ */
TCB_t *task_list_head = NULL;   // tête de liste chaînée
TCB_t *current_task_ptr = NULL; // tâche courante

/* ------------------ Fonctions interne ------------------ */
// Initialise les piles de chaque taches
void _init_task_stack(uint32_t **psp_addr, void (*task_handler)(void)) {
  uint32_t *pPSP = *psp_addr + STACK_SIZE;

  *(--pPSP) = DUMMY_XPSR;             // xPSR
  *(--pPSP) = (uint32_t)task_handler; // PC
  *(--pPSP) = 0xFFFFFFFD;             // LR, retour en Thread mode avec PSP

  // Initialisation registres R0-R12
  for (int i = 0; i < 13; i++) {
    *(--pPSP) = 0;
  }

  *psp_addr = pPSP; // Met à jour l'adresse PSP dans la TCB
}

// Sécurité dans le cas où il n'y aurait plus aucune tâche
void _idle_task(void) {
  while (1)
    ;
}

void _add_task_to_list(TCB_t *new_task) {
  // Sécurité : la liste ne doit jamais être vide, _idle_task doit exister
  if (task_list_head == NULL) {
    // Créer automatiquement _idle_task
    TCB_t *idle = (TCB_t *)malloc(sizeof(TCB_t));
    if (!idle)
      return;

    idle->psp_addr = (uint32_t *)malloc(STACK_SIZE * sizeof(uint32_t));
    if (!idle->psp_addr) {
      free(idle);
      return;
    }

    idle->current_state = TASK_READY_STATE;
    idle->block_count = 0;
    idle->task_handler = _idle_task;
    _init_task_stack(&idle->psp_addr, _idle_task);

    task_list_head = idle;
    idle->next = idle;       // liste circulaire
    current_task_ptr = idle; // pointe sur idle
  }

  // Ajout à la fin de la liste
  TCB_t *tmp = task_list_head;
  while (tmp->next != task_list_head) {
    tmp = tmp->next;
  }
  tmp->next = new_task;
  new_task->next = task_list_head;
}

/* ------------------ Fonctions externe ------------------ */
// update_next_task
void _scheduler(void) {
  if (!current_task_ptr) {
    current_task_ptr = task_list_head; // première tâche
  } else {
    TCB_t *start = current_task_ptr;
    do {
      current_task_ptr = current_task_ptr->next;
      if (current_task_ptr->current_state == TASK_READY_STATE)
        return;
    } while (current_task_ptr != start);

    // Si aucune tâche prête, idle
    current_task_ptr = task_list_head; // ou idle task
  }
}

// Débloque les taches endormi
void _unblock_tasks(void) {
  if (!task_list_head)
    return;

  TCB_t *tmp = task_list_head;
  do {
    if (tmp->current_state == TASK_BLOCKED_STATE && tmp->block_count <= g_tick_count) {
      tmp->current_state = TASK_READY_STATE;
    }
    tmp = tmp->next;
  } while (tmp != task_list_head);
}

// Fonction qui endors une tache le temps précisé
void task_delay(uint32_t ms) {
  __disable_irq();

  if (current_task_ptr && current_task_ptr->task_handler != _idle_task) { // On touche pas à idle c'est une securite
    current_task_ptr->block_count = g_tick_count + ms;
    current_task_ptr->current_state = TASK_BLOCKED_STATE;

    // Déclenche PendSV pour basculement de tâche
    SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
  }

  __enable_irq();
}

void add_task(void (*task_handler)(void)) {
  // Allocation TCB
  TCB_t *new_task = (TCB_t *)malloc(sizeof(TCB_t));
  if (!new_task)
    return; // échec allocation

  // Allocation pile
  new_task->psp_addr = (uint32_t *)malloc(STACK_SIZE * sizeof(uint32_t));
  if (!new_task->psp_addr) {
    free(new_task);
    return;
  }

  // Initialisation TCB
  new_task->current_state = TASK_READY_STATE;
  new_task->block_count = 0;
  new_task->task_handler = task_handler;

  // Initialisation pile PSP
  _init_task_stack(&new_task->psp_addr, task_handler);

  // Ajouter à la liste chaînée (à la fin)
  _add_task_to_list(new_task);
}

void _init_tasks(void) {
  // Initialise la liste de tâches avec _idle_task
  task_list_head = NULL;
  current_task_ptr = NULL;

  // Crée automatiquement _idle_task
  add_task(_idle_task);
}

task.h :

#ifndef TASK_H
#define TASK_H

#include <stdint.h>
#include "../GPIO/gpio.h"

/* ------------------ Types ------------------ */
typedef enum {
    TASK_READY_STATE = 0,
    TASK_BLOCKED_STATE = 1,
} state_task;

typedef struct TCB{
  uint32_t *psp_addr;
  void (*task_handler)(void);
  uint32_t block_count;
  state_task current_state;

  struct TCB *next; 
} TCB_t;

extern uint32_t g_tick_count;

/* ------------------ TASK ------------------ */
#define IDLE_STACK_START 0x20001000
#define SCHED_STACK_START 0x20006000
#define STACK_SIZE         256 

// Fonction pour dépendance ordonnanceur.c
void _scheduler(void);
void _unblock_tasks(void);
void _init_tasks(void);

// Fonction à partager à l'utilisateur
void task_delay(uint32_t ms);
void add_task(void (*task_handler)(void));

#endif
SPI

spi.c :

#include "./spi.h"

#define AFR_NumberBitParPin 0x4 //  Cf RM0401 p155/763
#define AFR_OFFSET_HIGH 8       //  Cf RM0401 p155/763

#define SPI_AFR 0b0101 // SPI sur AF5, Cf RM0401 p143/763

void spi_cs_on(GPIO_TypeDef *GPIOx, uint8_t PINx) { offPin(GPIOx, PINx); }
void spi_cs_off(GPIO_TypeDef *GPIOx, uint8_t PINx) { onPin(GPIOx, PINx); }

volatile uint8_t spi_lock;

void spiInit() {
  // SPI configuration instruction Cf RM0401 p682/763

  // STEP 1: Write proper GPIO registers: Configure GPIO for MOSI, MISO and SCK
  // pins.

  // PB15, MOSI
  setupPin(GPIOB, 15, ALTERNATE_FUNCTION);
  GPIOB->AFR[1] &= ~(0b1111 << ((15 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin)); // Clear
  GPIOB->AFR[1] |= SPI_AFR << ((15 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin);   // AFR[1] = AFRH

  // PB14, MISO
  setupPin(GPIOB, 14, ALTERNATE_FUNCTION);
  GPIOB->AFR[1] &= ~(0b1111 << ((14 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin)); // Clear
  GPIOB->AFR[1] |= SPI_AFR << ((14 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin);   // AFR[1] = AFRH

  // PB13, SCK
  setupPin(GPIOB, 13, ALTERNATE_FUNCTION);
  GPIOB->AFR[1] &= ~(0b1111 << ((13 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin)); // Clear
  GPIOB->AFR[1] |= SPI_AFR << ((13 - AFR_OFFSET_HIGH) * AFR_NumberBitParPin);   // AFR[1] = AFRH

  // STEP 2 : Write to the SPI_CR1 register:

  // ACTIVER L'HORLOGE AVANT TOUT SINON NE MARCHE
  RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;

  SPI2->CR1 = 0; // Reset tout

  // Cf RM0401 page 711/763 le tableau des états

  // LES SPECS DU SPI
  // https://learn.sparkfun.com/tutorials/using-the-serial-7-segment-display/all

  // a) Configure the serial clock baud rate using the BR[2:0] bits (Note: 3).
  SPI2->CR1 |= 0b101 << SPI_CR1_BR_Pos; // 101 : fPCLK/64

  // fPCLK /32 fait 250kHz car fpclk = 8MHz et
  // on à 250kHz maximum clock, cf sparkfun spec
  // On prend alors en dessous car sinon des
  // erreurs viennent se glisser pendant l'envoie

  //  b) Configure the CPOL and CPHA bits combination to define one of the four
  //  relationships between the data transfer and the serial clock. (Note: 2 -
  //  except the case when CRC is enabled at TI mode).
  SPI2->CR1 &= ~(1 << SPI_CR1_CPOL_Pos); // 0 : 0 when idle
  // And, data is clocked in on the rising edge of the clock (when it goes from
  // 0V to 5V).

  SPI2->CR1 &= ~(1 << SPI_CR1_CPHA_Pos); // 0 :
  // first clock transition is first data capture edge

  // c) Select simplex or half-duplex mode by configuring RXONLY or BIDIMODE and
  // BIDIOE (RXONLY and BIDIMODE can't be set at the same time).
  SPI2->CR1 &= ~(1 << SPI_CR1_RXONLY_Pos); // 0 : full-duplex

  // d) Configure the LSBFIRST bit to define the frame format (Note: 2).
  SPI2->CR1 &= ~(1 << SPI_CR1_LSBFIRST_Pos); // 0 : MSB transmitted first

  // e) Configure the CRCEN and CRCEN bits if CRC is needed (while SCK clock
  // signal is at idle state).
  SPI2->CR1 &= ~(1 << SPI_CR1_CRCEN_Pos); // 0: CRC calculation disabled

  // f) Configure SSM and SSI (Note: 2).
  // When the SSM bit is set, the NSS pin input is replaced with the value from
  // the SSI
  SPI2->CR1 |= (1 << SPI_CR1_SSM_Pos); // Software slave management
  SPI2->CR1 |= (1 << SPI_CR1_SSI_Pos); //

  // g) Configure the MSTR bit (in multimaster NSS configuration, avoid conflict
  // state on NSS if master is configured to prevent MODF error).
  SPI2->CR1 |= 1 << SPI_CR1_MSTR_Pos; // 1 : Master configuration

  // Data frame format
  SPI2->CR1 &= ~(1 << SPI_CR1_DFF_Pos); // 0: 8-bit data frame format is
                                        // selected for transmission/reception
  // Enable SPI
  SPI2->CR1 |= 1 << SPI_CR1_SPE_Pos; // 1 : Peripheral enabled
}

uint8_t spi_write(uint8_t data, GPIO_TypeDef *CS_GPIOx, uint8_t CS_PINx) {
  uint8_t dataRecu;

  while (spi_lock)
    ;
  spi_lock = 1;

  // Activer CS
  spi_cs_on(CS_GPIOx, CS_PINx);

  // Attendre que TXE soit prêt
  while (!(SPI2->SR & SPI_SR_TXE))
    ;

  // Envoyer la donnée
  SPI2->DR = data;

  // Attendre que le buffer RXNE soit plein pour lire et vider
  while (!(SPI2->SR & SPI_SR_RXNE))
    ;
  dataRecu = SPI2->DR;

  // Désactiver CS
  spi_cs_off(CS_GPIOx, CS_PINx);

  spi_lock = 0;
  return dataRecu;
}

spi.h :

#pragma once

#include <stdint.h>
#include "../GPIO/gpio.h"

void spiInit();
void spi_cs_on(GPIO_TypeDef *GPIOx, uint8_t PINx);
void spi_cs_off(GPIO_TypeDef *GPIOx, uint8_t PINx);

uint8_t spi_write(uint8_t data, GPIO_TypeDef *CS_GPIOx, uint8_t CS_PINx);
USART

Bibliothèque permettant de gérer proprement l'UART et afficher et lire des caractères via le port série d'un PC.

usart.c :

#include "usart.h"
#include "../GPIO/gpio.h"

#define RX_BUFFER_SIZE 64

volatile char rx_buffer[RX_BUFFER_SIZE];
volatile uint8_t rx_head = 0;
volatile uint8_t rx_tail = 0;

void usart_init(uint32_t baudrate) {
  setupPin(GPIOA, 9, ALTERNATE_FUNCTION_USART);  // PA9 => TX
  setupPin(GPIOA, 10, ALTERNATE_FUNCTION_USART); // PA10 => RX

  // Evidemment on active l'horloge du bus
  RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

  // Procedure (Cf STM32F410 p.627/763):
  // 2. Program the M bit in USART_CR1 to define the word length.
  USART1->CR1 = 0; // M=0, 1 Start bit, 8 Data bits, n Stop bit

  // 3. Program the number of stop bits in USART_CR2.
  USART1->CR2 = 0;

  // 4. Select DMA enable (DMAR) in USART_CR3 if multibuffer communication is to take
  // place. Configure the DMA register as explained in multibuffer communication. STEP 3
  USART1->CR3 = 0;

  // 5. Select the desired baud rate using the baud rate register USART_BRR
  uint32_t usartclk = 16000000;      // APB2 ~16MHz (Nucleo F4)
  USART1->BRR = usartclk / baudrate; // BRR : baudrate = fclk / USARTDIV

  // 6. Set the RE bit USART_CR1. This enables the receiver that begins searching for a start
  // bit.
  USART1->CR1 |= USART_CR1_PS; // Parity selection, 0 = Even parity

  // 1. Enable the USART by writing the UE bit in USART_CR1 register to 1.
  // Bon la datasheet dis etape 1 mais faut vraiment le faire à la fin l'activation sinon marche pas
  USART1->CR1 |= USART_CR1_TE | USART_CR1_RE; // TX & RX

  USART1->CR1 |= USART_CR1_RXNEIE; // activer interruption RX

  USART1->CR1 |= USART_CR1_UE; // USART

  NVIC_EnableIRQ(USART1_IRQn);
}

void usart_send_char(char c) {
  while (!(USART1->SR & USART_SR_TXE))
    ;
  USART1->DR = (c & 0xFF);
}

void usart_print(char *s) {
  while (*s) { // Tant que le caractère != '\0'
    usart_send_char(*s);
    s++;
  }
}

void USART1_IRQHandler() {
  if (USART1->SR & USART_SR_RXNE) {
    char c = USART1->DR & 0xFF;
    uint8_t next = (rx_head + 1) % RX_BUFFER_SIZE;

    if (next != rx_tail) { // buffer pas plein
      rx_buffer[rx_head] = c;
      rx_head = next;
    }
  }
}

int usart_buffer_available() {
  return (rx_head != rx_tail);
}

char usart_read() {
  if (rx_head == rx_tail)
    return 0; // rien dispo

  char c = rx_buffer[rx_tail];
  rx_tail = (rx_tail + 1) % RX_BUFFER_SIZE;
  return c;
}

usart.h :

#ifndef USART_H
#define USART_H

#include "../../../00-lib/stm32f410rx.h"

void usart_init(uint32_t baudrate);
void usart_send_char(char c);
void usart_print(char *s);
void usart_print_c(char c);

int usart_buffer_available(void);
char usart_read(void);

#endif
Substitute

Ce code à pour but de simplifier l'implémentation de la carte clavier ou la carte écran à moyen et long terme.

printf.h :

#ifndef PRINTF_H
#define PRINTF_H

#include "../USART/usart.h"

#define PRINT_STRING(str)        usart_print((char*)str)
#define PRINT_CHAR(c)        usart_send_char(c)

#define GETCHAR()          usart_read()
#define WAITCHAR()          !usart_buffer_available()

#endif

Grâce à cette fonction macro on pourra changer facilement la dépendance USART en combinaison carte clavier et/ou écran.

Carte fille Clavier

Hardware

Boutons utilisés

Nous voulions implémenter un clavier à touches mécaniques. Monsieur Redon avait des switchs qui convenait donc nous n'avions pas besoin d'en recommander.

Les switchs sont de la marque KAILH :

Boite Kailh
Boite Kailh Switch

Petit bémol : les sockets hot-swap ne correspondaient pas à nos boutons. Comme on peut le voir sur la photo de droite, il existe deux types de sockets pour nos modules hot-swap. Nous avons donc dû commander les modèles adaptés.

Kailh Hot swap socket
Bouton kailh comparaison hot swap socket

L’intérêt de ces modules est de pouvoir insérer ou retirer les boutons à volonté, sans qu’ils soient soudés directement au PCB — ce sont les sockets qui, eux, sont soudés. Cette solution présente plusieurs avantages : elle facilite la réutilisation des boutons d’un projet à un autre et améliore la réparabilité du clavier.

Concevons un clavier !

Notre clavier doit comporter 62 touches, conformément au format standard ISO 60 %, et sera capable d’assurer l’ensemble des combinaisons de touches attendues pour un clavier moderne en 2025.

Afin de customiser notre clavier, on se rend sur le site keyboard-layout-editor .

Nous pouvons partir d'un modèle de base ou alors d'un preset :

Site keyboard layout preset ISO 60%

Nous nous sommes alors inspirés des claviers disponibles sur le marché afin d’adopter un placement des touches conforme aux dispositions les plus courantes.

Keyboard layout

L’utilisation de cet outil présente plusieurs intérêts : elle permet d’imaginer et définir la disposition du clavier, de le découper en lignes et colonnes afin de concevoir la matrice de touches, et enfin d’identifier les bonnes empreintes à utiliser sur le futur PCB grâce au sommaire illustré ci-dessous.

Summary keyboard layout

Ce sommaire indique la taille indicative de chaque type de touche ainsi que le nombre de touches associées à chacune d’elles. Le "coloriage" est utile pour voir visuellement quelle touche correspond à quelle taille. On peut également sauvegarder notre configuration en exportant sous format "json". Via ce format on peut utiliser une extension de kicad qui se prénomme "Keyboard footprints placer" et qui permet de placer automatiquement les boutons si on les intancie dans le bon ordre (exemple : bouton 1 => SW1 , etc...). L'outil est un peu capricieux mais fait gagner un temps précieux sur le routage.

Schématique

Notre carte fille comporte plusieurs éléments :

  1. Le microcontrôleur ATMega32U4 avec un cristal de 16 MHz, des capacités de découplage et une ferrite (Cf AVR042) ;
  2. L'USB pour la programmation et l'alimentation pendant la phase programmation du projet ;
  3. Le connecteur ISP ;
  4. Les boutons RST et HWB ;
  5. Le connecteur SPI pour la communication avec la carte mère ;
  6. La led pour l'alimentation de la carte ;
  7. La led pour l'état du clavier (rôle ?) ;
  8. La matrice de touches évidemment ;
  9. Des mounting holes.

Remarque : Pas de leds RGB, pas assez de pins et nous ne voulions pas nous éparpiller sur trop d'idées (sujet evoqué avec Monsieur Boé).

Clavier schematique
Clavier schematique

Vue 3D

Keyboard 3D up
Keyboard 3D back

Brasure

Nous avons soudé le strict minimum sur notre carte pour le faire fonctioner avant tout puisque que nous souhaitons trouver les éventuelles anomalies de soudure ou de conception avant chaque grosse étape. Sur la PCB rouge et la 1ère pcb verte tout est ok.

clavier brasé
clavier brasé

La seconde PCB verte en revanche n'est pas reconnu. On teste les différentes connexions au multimètre. Le 5V est bien là. On teste alors le quartz à l'oscilloscope qui semble donner du bruit dans le cas de notre pcb dysfonctionnelle. Cependant après avoir changé le quartz, le problème est toujours présent. On finit alors par se rendre compte que l'on a inversé une capacité avec une resistance, le problème est donc résolu rapidement.

test_oscillo
test_oscillo
oscillo_vert
oscillo_vert
oscillo_rouge
oscillo_rouge

Software

Test Led

Afin de vérifier que notre clavier fonctionne, on fait un test afin de faire clignoter nos deux leds : led d'alimentation et led pour Cap Lock (qui nous servira par la suite pour savoir si notre carte est en mode majuscule ou non).

Remarque : la fonction setupPin est la même que celle présentée dans la section ordonnanceur de la carte shield.

#define F_CPU 16000000UL

#define LEDs_PORT PORTE
#define LEDs_DDR DDRE
#define LEDs_PIN PINE
#define LED_CapsLock PE6

void setupHardware() {
  setupClock();
  // Leds
  setupPin(&LEDs_PORT, &LEDs_DDR, LED_CapsLock, OUTPUT);

  // Permet de liberer le portF pour utiliser les boutons !
  MCUCR |= (1 << JTD); // 1ère écriture
  MCUCR |= (1 << JTD); // Désactiver JTAG (2ème écriture obligatoire !)
}

int main() {
  setupHardware();
  while (1) {
      LEDs_PORT |= (1 << LED_CapsLock); // toggle LED
      _delay_ms(500);
      LEDs_PORT &= ~(1 << LED_CapsLock); // toggle LED
      _delay_ms(500);
  }
    return 0;
}

Détection de notre matrice de boutons

On créer un fichier qui pourra être facilement importé dans nos différents projets afin d'avoir une détection de touches portable pour la suite : la LUFA.

Voici alors clavier.c :

#include "clavier.h"

#include "../lib/HARDWARE/hardware.h"
#include <avr/io.h>

// --------- Colonnes ---------
volatile uint8_t *col_ports[TOTAL_COL] = {
    [0 ... 5] = &PORTF,   // COL0 à COL5
    [6 ... 7] = &PORTC,   // COL6 à COL7
    [8 ... 10] = &PORTB,  // COL8 à COL10
    [11 ... 13] = &PORTD, // COL11 à COL13
};

volatile uint8_t *col_ddr[TOTAL_COL] = {
    [0 ... 5] = &DDRF,   // COL0 à COL5
    [6 ... 7] = &DDRC,   // COL6 à COL7
    [8 ... 10] = &DDRB,  // COL8 à COL10
    [11 ... 13] = &DDRD, // COL11 à COL13
};

volatile uint8_t *col_pins_reg[TOTAL_COL] = {
    [0 ... 5] = &PINF,   // COL0 à COL5
    [6 ... 7] = &PINC,   // COL6 à COL7
    [8 ... 10] = &PINB,  // COL8 à COL10
    [11 ... 13] = &PIND, // COL11 à COL13
};

uint8_t col_pins[TOTAL_COL] = {0, 1, 4, 5, 6, 7, 7, 6, 6, 5, 4, 7, 6, 4};

// --------- Lignes ---------
volatile uint8_t *row_ports[TOTAL_ROW] = {
    [0 ... 4] = &PORTD,
};

volatile uint8_t *row_ddr[TOTAL_ROW] = {
    [0 ... 4] = &DDRD,
};

uint8_t row_pins[TOTAL_ROW] = {5, 3, 2, 1, 0};

uint8_t key_state[TOTAL_COL][TOTAL_ROW] = {0};

void init_matrix_button(void) {
  // Configuration colonnes en entrée avec pull-up
  for (uint8_t c = 0; c < TOTAL_COL; c++) {
    setupPin(col_ports[c], col_ddr[c], col_pins[c], INPUT_PULL_UP);
  }

  // Configuration ligne en sortie
  for (uint8_t r = 0; r < TOTAL_ROW; r++) {
    setupPin(row_ports[r], row_ddr[r], row_pins[r], OUTPUT);
    onPin(row_ports[r], row_pins[r]); // mettre toutes les lignes à 1 pour les desactiver
  }
}

void scan() {
  for (uint8_t r = 0; r < TOTAL_ROW; r++) {
    offPin(row_ports[r], row_pins[r]); // activer ligne (LOW)

    for (uint8_t c = 0; c < TOTAL_COL; c++)
      key_state[c][r] = !(*col_pins_reg[c] & (1 << col_pins[c]));

    onPin(row_ports[r], row_pins[r]); // désactiver ligne (HIGH)
  }
}

Le scan se fait facilement une fois la logique assimilée... Et clavier.h :

#ifndef CLAVIER_H
#define CLAVIER_H

#include "keyswitch.h"
#include <stdint.h>
#include "clavier_conversion.h"

extern uint8_t key_state[TOTAL_COL][TOTAL_ROW];

void init_matrix_button(void);
void scan(void);

#endif

Et ensuite, nous avons plein de define afin de lire chaque bouton individuellement au lieu d'appeler un tableau (pas intuitif pour l'utilisateur) dans un fichier keyswitch.h :

#ifndef KEYSWITCH_H
#define KEYSWITCH_H

#include <stdint.h>

#define TOTAL_KEYSWITCH 62
#define TOTAL_COL 14
#define TOTAL_ROW 5


// COL0 PF0 | COL1 PF1 | COL2 PF4 | COL3  PF5 | COL4  PF6 | COL5  PF7 | COL6  PC7 | 
// COL7 PC6 | COL8 PB6 | COL9 PB5 | COL10 PB4 | COL11 PD7 | COL12 PD6 | COL13 PD4
typedef enum{
    COL0 = 0,
    COL1 = 1,
    COL2 = 2,
    COL3 = 3,
    COL4 = 4,
    COL5 = 5,
    COL6 = 6,
    COL7 = 7,
    COL8 = 8,
    COL9 = 9,
    COL10 = 10,
    COL11 = 11,
    COL12 = 12,
    COL13 = 13,
} COLs;

// ROW0 PD5 | ROW1 PD3 | ROW2 PD2 | ROW3 PD1 | ROW4 PD0
typedef enum{
    ROW0 = 0,
    ROW1 = 1,
    ROW2 = 2,
    ROW3 = 3,
    ROW4 = 4,
} ROWs;

extern uint8_t key_state[TOTAL_COL][TOTAL_ROW];

#define btn1  key_state[COL0][ROW0]
#define btn2  key_state[COL1][ROW0]
#define btn3  key_state[COL2][ROW0]
#define btn4  key_state[COL3][ROW0]
#define btn5  key_state[COL4][ROW0]
#define btn6  key_state[COL5][ROW0]
#define btn7  key_state[COL6][ROW0]
#define btn8  key_state[COL7][ROW0]
#define btn9  key_state[COL8][ROW0]
#define btn10 key_state[COL9][ROW0]
#define btn11 key_state[COL10][ROW0]
#define btn12 key_state[COL11][ROW0]
#define btn13 key_state[COL12][ROW0]
#define btn14 key_state[COL13][ROW0]

#define btn15 key_state[COL0][ROW1]
#define btn16 key_state[COL1][ROW1]
#define btn17 key_state[COL2][ROW1]
#define btn18 key_state[COL3][ROW1]
#define btn19 key_state[COL4][ROW1]
#define btn20 key_state[COL5][ROW1]
#define btn21 key_state[COL6][ROW1]
#define btn22 key_state[COL7][ROW1]
#define btn23 key_state[COL8][ROW1]
#define btn24 key_state[COL9][ROW1]
#define btn25 key_state[COL10][ROW1]
#define btn26 key_state[COL11][ROW1]
#define btn27 key_state[COL12][ROW1]
#define btn28 key_state[COL13][ROW1]

#define btn29 key_state[COL0][ROW2]
#define btn30 key_state[COL1][ROW2]
#define btn31 key_state[COL2][ROW2]
#define btn32 key_state[COL3][ROW2]
#define btn33 key_state[COL4][ROW2]
#define btn34 key_state[COL5][ROW2]
#define btn35 key_state[COL6][ROW2]
#define btn36 key_state[COL7][ROW2]
#define btn37 key_state[COL8][ROW2]
#define btn38 key_state[COL9][ROW2]
#define btn39 key_state[COL10][ROW2]
#define btn40 key_state[COL11][ROW2]
#define btn41 key_state[COL12][ROW2]

#define btn42 key_state[COL0][ROW3]
#define btn43 key_state[COL1][ROW3]
#define btn44 key_state[COL2][ROW3]
#define btn45 key_state[COL3][ROW3]
#define btn46 key_state[COL4][ROW3]
#define btn47 key_state[COL5][ROW3]
#define btn48 key_state[COL6][ROW3]
#define btn49 key_state[COL7][ROW3]
#define btn50 key_state[COL8][ROW3]
#define btn51 key_state[COL9][ROW3]
#define btn52 key_state[COL10][ROW3]
#define btn53 key_state[COL11][ROW3]
#define btn54 key_state[COL13][ROW3]

#define btn55 key_state[COL0][ROW4]
#define btn56 key_state[COL1][ROW4]
#define btn57 key_state[COL2][ROW4]
#define btn58 key_state[COL6][ROW4]
#define btn59 key_state[COL10][ROW4]
#define btn60 key_state[COL11][ROW4]
#define btn61 key_state[COL12][ROW4]
#define btn62 key_state[COL13][ROW4]

#endif

La bibliothèque à été pensée pour être facilement réadaptable et lisible pour un utilisateur souhaitant re-coder un clavier mais également pour quelqu'un ne souhaitant pas refaire l'hardware et juste coder/utiliser le clavier existant.

LUFA

Afin que nos touches soient reconnues comme des lettres de l'alphabet que l'on peut voir sur notre écran, on utilise la LUFA. Rien de bien compliqué, on retrouve le projet LUFA sur ce lien github : https://github.com/abcminiuser/lufa

On télécharge le projet et on vient extraire la librairie LUFA et l'exemple Keyboard présent dans le dossier Démo. On modifie le Makefile présent ainsi :

MES_LIBS = lib/HARDWARE/hardware.c lib/CLAVIER/clavier.c

MCU          = atmega32u4
ARCH         = AVR8
BOARD        = NONE
F_CPU        = 16000000
F_USB        = $(F_CPU)
OPTIMIZATION = s
TARGET       = Keyboard
SRC          = $(TARGET).c Descriptors.c $(MES_LIBS) $(LUFA_SRC_USB) $(LUFA_SRC_USBCLASS)
LUFA_PATH    = ../LUFA
CC_FLAGS     = -DUSE_LUFA_CONFIG_HEADER -IConfig/
LD_FLAGS     =

# Default target
all:

# Include LUFA-specific DMBS extension modules
DMBS_LUFA_PATH ?= $(LUFA_PATH)/Build/LUFA
include $(DMBS_LUFA_PATH)/lufa-sources.mk
include $(DMBS_LUFA_PATH)/lufa-gcc.mk

# Include common DMBS build system modules
DMBS_PATH      ?= $(LUFA_PATH)/Build/DMBS/DMBS
include $(DMBS_PATH)/core.mk
include $(DMBS_PATH)/cppcheck.mk
include $(DMBS_PATH)/doxygen.mk
include $(DMBS_PATH)/dfu.mk
include $(DMBS_PATH)/gcc.mk
include $(DMBS_PATH)/hid.mk
include $(DMBS_PATH)/avrdude.mk
include $(DMBS_PATH)/atprogram.mk

PROGRAMMER = avrdude
AVRDUDE_PORT = /dev/ttyACM0
AVRDUDE_BAUD = 115200
AVRDUDE_PROGRAMMER = avr109

upload: $(TARGET).hex
	$(PROGRAMMER) -v -p $(MCU) -c $(AVRDUDE_PROGRAMMER) \
		-P $(AVRDUDE_PORT) -b $(AVRDUDE_BAUD) -D \
		-U flash:w:$(TARGET).hex:i

La ligne upload est nécessaire seulement si vous avez un bootloader personnalisé, auquel cas faire un simple make dfu pour téléverser. Ensuite on vient ajouter nos libraries et notre code de logique aux fichiers Keyboard.c :

#include "./lib/CLAVIER/clavier.h"
#include "./lib/HARDWARE/hardware.h"

// ... je passe les détails basique du fichier

void SetupHardware() {
  /* Disable watchdog if enabled by bootloader/fuses */
  MCUSR &= ~(1 << WDRF);
  wdt_disable();

  /* Disable clock division */
  clock_prescale_set(clock_div_1);

  /* Hardware Initialization */
  setupHardware();
  init_matrix_button();
  
  // Initialisation USB obligatoire
  USB_Init();
}

// ... je passe les détails basique du fichier

bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t *const HIDInterfaceInfo,
                                         uint8_t *const ReportID,
                                         const uint8_t ReportType,
                                         void *ReportData,
                                         uint16_t *const ReportSize) {

  USB_KeyboardReport_Data_t *KeyboardReport = (USB_KeyboardReport_Data_t *)ReportData;
	uint8_t UsedKeyCodes = 0;

  scan();
  
     // ROW0
    if (btn1)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_ESCAPE);
    if (btn2 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_1_AND_EXCLAMATION);
    if (btn3 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_2_AND_AT);
    if (btn4 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_3_AND_HASHMARK);
    if (btn5 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_4_AND_DOLLAR);
    if (btn6 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_5_AND_PERCENTAGE);
    if (btn7 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_6_AND_CARET);
    if (btn8 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_7_AND_AMPERSAND);
    if (btn9 && !btn62)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_8_AND_ASTERISK);
    if (btn10 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_9_AND_OPENING_PARENTHESIS);
    if (btn11 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_0_AND_CLOSING_PARENTHESIS);
    if (btn12 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_MINUS_AND_UNDERSCORE); // ) ° ]
    if (btn13 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_EQUAL_AND_PLUS); // = + }
    if (btn14) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_BACKSPACE);

     // ROW1
    if (btn15) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_TAB);
    if (btn16) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_A);
    if (btn17) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Z);
    if (btn18) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_E);
    if (btn19) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_R);
    if (btn20) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_T);
    if (btn21) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Y);
    if (btn22) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_U);
    if (btn23) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_I);
    if (btn24) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_O);
    if (btn25) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_P);
    if (btn26) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_OPENING_BRACKET_AND_OPENING_BRACE); // ^¨
    if (btn27) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_CLOSING_BRACKET_AND_CLOSING_BRACE); // $£¤
    if (btn28) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_ENTER);

     // ROW2
    if (btn29) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_CAPS_LOCK);
    if (btn30) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_Q);
    if (btn31) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_S);
    if (btn32) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_D);
    if (btn33) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F); 
    if (btn34) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_G);
    if (btn35) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_H);
    if (btn36) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_J);
    if (btn37) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_K);
    if (btn38) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_L);
    if (btn39) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_M);
    if (btn40) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_APOSTROPHE_AND_QUOTE); // % ù
    if (btn41) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_NON_US_HASHMARK_AND_TILDE); // µ * 

     // ROW3
    if (btn42) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_SHIFT);
    if (btn43) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_NON_US_BACKSLASH_AND_PIPE);
    if (btn44) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_W);
    if (btn45) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_X);
    if (btn46) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_C);
    if (btn47) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_V);
    if (btn48) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_B);
    if (btn49) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_N);
    if (btn50) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN); // ,?
  if (btn51)
    KeyboardReport->KeyCode[UsedKeyCodes++] = HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN; // ;.
    if (btn52) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_DOT_AND_GREATER_THAN_SIGN); // :/
    if (btn53 && !btn62) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_SLASH_AND_QUESTION_MARK);  // !§
    if (btn54) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_RIGHT_SHIFT);

  // ROW4
  if (btn55) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_LEFTCTRL);
  if (btn56) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_LEFTGUI);
  if (btn57) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_ALT);
  if (btn58) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_SPACE);
  if (btn59 && !btn62) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_RIGHTALT);
  // if (btn60 && !btn62) KeyboardReport->Keyboard |= QWERTY_to_AZERTY(); // Trouver une fonction a celui ci
  if (btn61 && !btn62) KeyboardReport->Modifier |= QWERTY_to_AZERTY(HID_KEYBOARD_MODIFIER_RIGHTCTRL);

  // Fonction spéciale du clavier côté HARDWARE
  if(btn62){
    // Fonction F1 à F12
    if (btn2)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F1);
    if (btn3)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F2);
    if (btn4)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F3);
    if (btn5)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F4);
    if (btn6)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F5);
    if (btn7)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F6);
    if (btn8)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F7);
    if (btn9)  KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F8);
    if (btn10) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F9);
    if (btn11) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F10);
    if (btn12) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F11);
    if (btn13) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_F12);

    // Déplacement
    if (btn60) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_DOWN_ARROW);
    if (btn59) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_LEFT_ARROW);
    if (btn61) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_RIGHT_ARROW);
    if (btn53) KeyboardReport->KeyCode[UsedKeyCodes++] = QWERTY_to_AZERTY(HID_KEYBOARD_SC_UP_ARROW);

  }

  *ReportSize = sizeof(USB_KeyboardReport_Data_t);
  return false;
}

void CALLBACK_HID_Device_ProcessHIDReport(USB_ClassInfo_HID_Device_t *const HIDInterfaceInfo,
                                          const uint8_t ReportID,
                                          const uint8_t ReportType,
                                          const void *ReportData,
                                          const uint16_t ReportSize) {
  
  uint8_t *LEDReport = (uint8_t *)ReportData;

  if (*LEDReport & HID_KEYBOARD_LED_CAPSLOCK)
    onPin(LEDs_PORT,LED_CapsLock);
  else
    offPin(LEDs_PORT,LED_CapsLock);

}

J'ai fais une petite macro (reprise de l'année dernière, projet manette) pour convertir les caractères sur un clavier AZERTY dans le fichier clavier_conversion.h :

#ifndef CLAVIER_CONVERSION_H
#define CLAVIER_CONVERSION_H

#include <../../LUFA/Drivers/USB/USB.h>

static inline uint8_t QWERTY_to_AZERTY(uint8_t qwerty_code) {
    switch (qwerty_code) {
        // Lettres
        case HID_KEYBOARD_SC_Q: return HID_KEYBOARD_SC_A;
        case HID_KEYBOARD_SC_W: return HID_KEYBOARD_SC_Z;
        case HID_KEYBOARD_SC_A: return HID_KEYBOARD_SC_Q;
        case HID_KEYBOARD_SC_Z: return HID_KEYBOARD_SC_W;
        case HID_KEYBOARD_SC_M: return HID_KEYBOARD_SC_SEMICOLON_AND_COLON; 
        case HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN: return HID_KEYBOARD_SC_M;         
        default: return qwerty_code; 
    }
}

static inline uint8_t AZERTY_to_QWERTY(uint8_t azerty_code) {
    switch (azerty_code) {
        // Lettres
        case HID_KEYBOARD_SC_A: return HID_KEYBOARD_SC_Q;
        case HID_KEYBOARD_SC_Z: return HID_KEYBOARD_SC_W;
        case HID_KEYBOARD_SC_Q: return HID_KEYBOARD_SC_A;
        case HID_KEYBOARD_SC_W: return HID_KEYBOARD_SC_Z;
        case HID_KEYBOARD_SC_SEMICOLON_AND_COLON: return HID_KEYBOARD_SC_M; 
        case HID_KEYBOARD_SC_M: return HID_KEYBOARD_SC_COMMA_AND_LESS_THAN_SIGN;
        default: return azerty_code;
    }
}

#endif

J'ai eu beaucoup de mal à trouver certains caractères mais le document présent ci-dessous m'a grandement aidé :

HID Usage Tables

A partir de la page 89 nous avons l'ensemble des codes et détails pour certains d'entre eux. Malgré tout une touche n'as pas réussi à être mappée (je n'ai pas trouvé le code équivalent pour mon clavier AZERTY), cette touche correspond au point virgule / point (;.) (Bouton 51). Update : Dernier bouton trouvé :) Clavier entierement mappé !

Cependant le code reste fonctionnelle, j'y ai ajouté la possiblité de se déplacer via la touche FN qui correspond aux fonctionnalités spéciales, comme toutes les touches F1,F2,...F10,F11,F12 qui sont mappées aux touches 1,2,...,0,°,+.

Communication carte mère

Cette partie nécessite d'être travaillée mais servira de point de départ pour les prochaines années je l'espère.

Dans un dossier SPI à part nous avons ajouté un fichier cmd.h :

#ifndef CMD_H
#define CMD_H

#include <stdint.h>

#define CMD_NOCMD 0x00
#define CMD_ACK_SLAVE 0xFF

// --------------  Detection presence carte fille --------------
#define CMD_PING 0xAA
#define CMD_PING_REPLY 0x55

// --------------  Identification carte fille --------------
#define CMD_IDENTIFY 0x10

#define ID_KEYBOARD 0x01
#define ID_ECRAN 0x02
#define ID_SON 0x03
#define ID_RESEAU 0x04
#define ID_FPGA 0x05

// -------------- Specifique Keyboard --------------
#define CMD_READ_EVENT 0x20

// -------- Structure keycode -------- //
typedef struct {
  char key;        // lettre correspondante
  uint8_t pressed; // 1 = press, 0 = release
} key_event_t;

#endif

Il sert à simplifier l'implémentation des commandes qui sont communes à tous les groupes. spi.c :

#include "spi.h"
#include <avr/io.h>
#include <avr/interrupt.h>

// -------- SPI avec carte mere -------- //
#define INT_PORT PORTB
#define INT_DDR  DDRB
#define INT_PIN  PB0

#define INT_LOW()   (INT_PORT &= ~(1 << INT_PIN))
#define INT_HIGH()  (INT_PORT |=  (1 << INT_PIN))

static volatile uint8_t spi_state = 0;
static volatile key_event_t current_event;

// -------- Buffer circulaire -------- //
#define SPI_BUFFER_SIZE 16
static volatile key_event_t buffer[SPI_BUFFER_SIZE];
static volatile uint8_t head = 0;
static volatile uint8_t tail = 0;


static inline uint8_t __bufferIsEmpty(void) {
    return head == tail;
}

static inline void __bufferPush(key_event_t ev) {
    uint8_t next = (head + 1) % SPI_BUFFER_SIZE;

    if (next == tail) {
        tail = (tail + 1) % SPI_BUFFER_SIZE; // overwrite
    }

    buffer[head] = ev;
    head = next;
}

static inline uint8_t __bufferPop(volatile key_event_t *ev) {
    if (__bufferIsEmpty())
        return 0;

    *ev = buffer[tail];
    tail = (tail + 1) % SPI_BUFFER_SIZE;
    return 1;
}

// -------- SPI -------- //
void spi_init(void) {
    /* Config SPI Slave : MISO => output, MOSI/SCK/SS => input */
    DDRB |=  (1 << PB3);  // MISO
    DDRB &= ~((1 << PB2) | (1 << PB1) | (1 << PB0)); // MOSI, SCK, SS

    /* SPI enable + interruption */
    SPCR = (1 << SPE) | (1 << SPIE);

    /* INT pin */
    INT_DDR |= (1 << INT_PIN);
    INT_HIGH();

    SPDR = CMD_ACK_SLAVE; // On precharge ack pour le 1er cycle, a chaque reset c'est utile
}

ISR(SPI_STC_vect) {
    uint8_t rx = SPDR;
    uint8_t tx = CMD_NOCMD;

    switch (rx) {
        case CMD_PING:
            tx = CMD_PING_REPLY;
            break;

        case CMD_IDENTIFY:
            tx = ID_KEYBOARD;
            break;

        case CMD_READ_EVENT:
            if (__bufferPop(&current_event)) {
                tx = current_event.key;
                spi_state = 1; // prochain octet = pressed
            } else {
                tx = CMD_NOCMD;
                INT_HIGH(); // RAS pour la carte mere
            }
            break;

        default:
            if (spi_state == 1) {
                tx = current_event.pressed;
                spi_state = 0;

                if (__bufferIsEmpty())
                    INT_HIGH(); // RAS pour la carte mere
            }
            break;
    }

    SPDR = tx; // a envoyer au prochain cycle
}
// Manque a coder dautre primitive

spi.h :

#ifndef SPI_H
#define SPI_H

#include "cmd.h"

void spi_init(void);
void spi_push_event(char key, uint8_t pressed);

#endif

Carte Fille FPGA

Objectif

Une carte FPGA est actuellement en développement en parallèle. Il s’agit d’un défi technique majeur visant à faire évoluer le projet de pico-ordinateur vers une nouvelle étape.

L’objectif, à terme, est de concevoir un pico-ordinateur complet capable de gérer des flux vidéo et audio, ainsi que différents protocoles HID, notamment en intégrant un microcontrôleur (comme sur la carte Nexys A7, par exemple). Dans cette optique, le développement de la carte FPGA doit progresser au mieux, mais il est possible que le travail se poursuive sur le semestre S8, comme convenu avec M. Boé.

Schématique

Les notes liées à la conception de la schématique se trouvent dans ce répertoire : https://gitea.plil.fr/ahouduss/SE4-Pico-B6/src/branch/master/01-Kicad/Recherche%20FGPA .

La schématique comporte elle même toutes les explications, il est donc inutile de revenir sur chacun de ces points ici.

Puce

La référence de la puce FPGA à router est celle ci : XC7A15T-1FTG256C. C'est une puce de la famille Artix-7. La puce FPGA étant assez complexe, elle ne peut être représentée en une seule entité sous kicad, elle est donc décomposée dans les unités suivantes :

  • U1A dans la sheet FPGA_Banks_14-15
  • U1B dans la sheet FPGA_Banks_34-35
  • U1C dans la sheet FPGA_Config
  • U1D dans la sheet FPGA_Power

Liste et Description des sheets

FPGA_Power :

Feuille regroupant l'alimentation critique de notre puce :

  1. VCCAUX : Auxiliary voltage (tension auxiliaire), alimente circuits internes non critiques en puissance.
  2. VCCINT : Internal core voltage (tension interne du cœur logique), alimente la logique principale du FPGA (LUTs, Flip-flops).
  3. VCCADC/BATT :
    • VCCADC : tension pour le module ADC si le FPGA en a un.
    • VBATT : alimenter registres de configuration non volatiles ou horloge temps réel.
  4. VCCBRAM : Block RAM, alimente les blocs mémoire. Cela permet de séparer l’alimentation de la mémoire afin de réduire le bruit.

FPGA_Banks_14-15 et FPGA_Banks_34-35 : Feuille disposant de l'ensemble des entrées et sorties du FPGA n'ayant pas de fonction prédisposée donc libre pour ajouter nos composants.

FPGA_Config : Feuille regroupant les pins de programmation de la puce en fonction du mode choisi au préalable ainsi que la logique data USB-C (2.0 ici) .

Switch&Button : Feuille contenant les boutons et les switchs. Joue le rôle de la carte matrice de boutons.

LEDs&7seg : Feuille contenant les LEDs et le 7 segments.

Ethernet : Feuille contenant la logique Ethernet de notre carte. Joue le rôle de la carte réseau.

VGA : Feuille contenant toute la logique du VGA. Joue le rôle de la carte écran.

HDMI : Feuille abandonnée car non supportée par notre puce FPGA.

Power : Feuille sur la gestion de l'alimentation avec son séquençage.

Memory : Feuille contenant la mémoire SRAM de notre carte. Joue le rôle de la carte mémoire.