« SE4Binome2024-3 » : différence entre les versions
Ligne 57 : | Ligne 57 : | ||
==== carte mère nue ==== | ==== carte mère nue ==== | ||
[[Fichier: | [[Fichier:PCB_carte_mère_nu.jpg|sans_cadre]] | ||
== Programmation ordonnanceur == | == Programmation ordonnanceur == |
Version du 19 novembre 2024 à 13:48
Description
L'objectif de ce projet est de réaliser un pico ordinateur. Nous allons traiter le développement de la carte mère, un shield arduino ainsi que la programmation d'un ordonnanceur.
Notre carte mère sera alimenté via une alimentation secteur en 5v et le code sera implanté par USB.
Réalisation d'un shield arduino
Schématique et routage
![]() |
![]() |
---|
PINOUT
LED1 | PC0 |
LED2 | PC3 |
LED3 | PD1 |
LED4 | PD4 |
LED5 | PD7 |
Brasage du shield
shield nu
shield fini
Test du shield
Réalisation carte mère
Schématique et routage
Brasage carte mère
carte mère nue
Programmation ordonnanceur
Initialisation de la pile
void init_pile(int N){
uint16_t tempSP = SP;
SP = task[N].stackPointer;
uint16_t adresse=(uint16_t)task[N].addressFunction;
asm volatile("push %0" : : "r" (adresse & 0x00ff) );
asm volatile("push %0" : : "r" ((adresse & 0xff00)>>8) );
SAVE_REGISTERS();
task[N].stackPointer = SP;
SP = tempSP;
}
La fonction init_pile() sert à charger un processus dans la pile. Cette fonction est appelée dans une boucle au début du main pour initialiser tous les processus.
Interruption de l'ordonnanceur
L'ordonnanceur est appelé à un interval de temps régulier grâce à des interruptions du Timer1.
Avant de passer d'une tâche à une autre (mode Round Robin) il faut d'abord effectuer quelques opérations sur la mémoire du micro-processeur.
ISR(TIMER1_COMPA_vect, ISR_NAKED){ // Procédure d'interruption
/* Sauvegarde du contexte de la tâche interrompue */
SAVE_REGISTERS();
task[currentTask].stackPointer = SP;
/* Appel à l'ordonnanceur */
scheduler();
/* Récupération du contexte de la tâche ré-activée */
SP = task[currentTask].stackPointer;
RESTORE_REGISTERS();
asm volatile ( "reti" );
}
La routine d'interruption est générée par le Timer1 avec comme paramètre ISR_NAKED qui permet de ne pas avoir le prologue et l'épilogue automatique du compilateur ce qui nous est très utile pour gérer manuellement ces interruptions afin d'éxecuter nos tâches.
La macro SAVE_REGISTERS() nous permet de sauvegarder les 32 registres sur la pile. Cela permet de s’assurer qu’au retour de l'interruption, la tâche interrompue pourra reprendre son exécution là où elle s’était arrêtée, sans perdre son contexte.
task[currentTask].stackPointer = SP permet de sauvegarder le pointeur de pile de la tâche associée.
scheduler() est expliqué ici.
SP = task[currentTask].stackPointer permet de replacer le pointeur de pile à celui de la nouvelle tâche engendrée par le scheduler.
La macro RESTORE_REGISTERS() remets la nouvelle fonction sur les 32 registres avec des Pop().
asm volatile ( "reti" ) : étant en ISR_NAKED, il faut dire manuellement la fin de l'interruption avec cette commande assembleur RETurn from Interrupt.
Structure d'une tâche
enum States{
AWAKE,
IDLE
};
enum IDLE_TYPE{
IDLE_TYPE_DELAY,
IDLE_TYPE_STABLE
};
typedef union {
int sleeping_time;
} Time;
typedef struct {
enum IDLE_TYPE type;
Time time;
} Etat ;
typedef struct{
uint16_t stackPointer; // stack pointer, défini le début du process
void (*addressFunction)(void); // adresse de l'instruction en cours
enum States state; // état de la fonction (Awake ou Idle)
Etat etat;
}process;
Les tâches sont définies par leurs pointeurs de piles, leurs adresses, un état qui est soit IDLE ou AWAKE et enfin un dernier état qui lui gère permet d'avoir différent type d'IDLE.
L'utilisation d'unions n'est pas forcément justifiée ici mais permet de limiter la place que va prendre la structure dans l'optique de futures implémentations.
Code de l'ordonnanceur
void scheduler(){
// décrémentation de l'attente de toutes les tâches en IDLE
for(int i=0;i<NB_TASKS;i++){
if(task[i].state==IDLE && (task[i].etat.type==IDLE_TYPE_DELAY)){
task[i].etat.time.sleeping_time -= PERIODE;
if(task[i].etat.time.sleeping_time <= 0){
task[i].etat.time.sleeping_time = 0;
task[i].state = AWAKE;
}
}
}
do{
currentTask ++;
if(currentTask >= NB_TASKS) currentTask = 0; // ordonnanceur en mode Round Robin
}while(task[currentTask].state == IDLE);
}
L'ordonnanceur est en mode Round Robin (tourniquet) ce qui veut dire qu'il n'y a pas de priorité de tâche, elles sont toutes appelées une à une dans l'ordre avant de boucler et ainsi de suite...
L'un des ajouts a été de mettre un mode IDLE à nos tâches qui indique la veille de ces dernières. Si une tâche est en IDLE, elle ne sera pas appelée lors de la sélection des tâches et son temps de sommeil sera décrémenté. Cela permet de créer des attentes passives donc d'éviter d'attendre inutilement des tâches qui ne font qu'attendre. On bon exemple pour comprendre serait avec un clignotement de LED. Au lieu de mettre un _delay_ms(1000) qui éxecutera une attente à chaque fois que cette tâche est appelée, on la passe pendant 1000 ms ce qui permet d'éxécuter d'autres tâches entre temps.
Fonction wait
void wait(int time_ms){
task[currentTask].state = IDLE;
task[currentTask].etat.type=IDLE_TYPE_DELAY;
task[currentTask].etat.time.sleeping_time = time_ms;
TCNT1 = 0;
TIMER1_COMPA_vect();
}
La fonction permet d'endormir un processus pendant un temps donné en ms.
Elle passe la tâche en IDLE, remet le Timer1 à zéro pour éviter de garder un décalage de temps car le changement est surement fait en plein milieu d'une tâche.
Et ensuite elle appelle l'interruption pour qu'une nouvelle tâche soit en cours.
Exemple de tâches
void USART_Init(unsigned int ubrr)
{
/*Set baud rate */
UBRR0H = (unsigned char)(ubrr>>8);
UBRR0L = (unsigned char)ubrr;
/*Enable receiver and transmitter */
UCSR0B = (1<<RXEN0)|(1<<TXEN0);
/* Set frame format: 8data, 2stop bit */
UCSR0C = (1<<USBS0)|(3<<UCSZ00);
}
void LED_init(){
DDRC |= (1<<LED1) | (1<<LED2);
DDRD |= (1<<LED3) | (1<<LED4) | (1<<LED5);
PORTC |= (1<<LED1)|(1<<LED2);
PORTD |= (1<<LED3)|(1<<LED4)|(1<<LED5);
}
void LED1_blink(){
while(1){
PORTC ^= (1<<LED1);
_delay_ms(1000);
}
}
void LED2_blink(){
while(1){
PORTC ^= (1<<LED2);
wait(100);
}
}
void Serial_Message(){
unsigned char data;
while(1){
data = USART_Receive();
USART_Transmit(data);
}
}
void USART_Transmit(unsigned char data)
{
/* Wait for empty transmit buffer */
while (!(UCSR0A & (1<<UDRE0)))
;
/* Put data into buffer, sends the data */
UDR0 = data;
}
unsigned char USART_Receive(void)
{
/* Wait for data to be received */
while (!(UCSR0A & (1<<RXC0)))
;
/* Get and return received data from buffer */
return UDR0;
}
void spi_activer(void){ // Activer le périphérique
PORTD &= ~(1<<SPI_SS); // Ligne SS à l'état bas
}
void spi_desactiver(void){ // Désactiver le périphérique
PORTD |= (1<<SPI_SS); // Ligne SS à l'état haut
}
uint8_t spi_echange(uint8_t envoi){ // Communication sur le bus SPI
SPDR = envoi; // Octet a envoyer
while(!(SPSR & (1<<SPIF))); // Attente fin envoi (drapeau SPIF du statut)
return SPDR; // Octet reçu
}
void seven_seg(){
while(1){
spi_activer();
spi_echange(0x01);
spi_desactiver();
}
}
Exemple d'utilisation
Clignotement de 5 Leds asynchrones
Affichage clavier avec minicom
Bilan de puissance
Afin d'être sûr de l'alimentation de toutes les cartes, nous utilisons une alimentation +5V d'une Raspberry PI branchée sur secteur.
|
|
|
---|
Liens
Lien du git : https://gitea.plil.fr/vdetrez/SE4_PICO_DETREZ_CART