L’exemple de cette page se base sur un microcontroleur PIC16F

Quelques règles de base

Ressources limitées

Les microcontroleurs sont des calculateurs simplifiés et dont la principale préoccupation est de répondre le plus rapidement possible à une sollicitation extérieure.

Ainsi, tout dans leur structure est fait pour améliorer le temps de réponse du microcontroleur et sa consommation énergétique. Les ressources internes sont alors restraintes. Pour vous en rendre compte, nous vous proposons de suivre les quelques étapes suivantes sur un PIC16F1503.

Si on s’intéresse à la documentation technique, on trouve un tableau de ce type en tout début :

L’attribut alt de cette image est vide, son nom de fichier est pic16f1503_carac.png.

Le PIC16F1503 peut stocker 2000 lignes de code en assembleur (4096 octets) et peut stocker jusqu’à 128 octets de variables.

Exemple de base

On se propose à présent d’étudier le morceau de code suivant :

#include 

void main( void ) {
int a ;
a = a +1;
}


Après l’avoir associé à un projet sous MPLABX (device : PIC16F1503 / compilateur : XC8), on peut compiler ce code et se rendre dans la section >>Window>>Dashboard. Une fenêtre s’ouvre alors en bas de l’interface de MPLABX :

L’attribut alt de cette image est vide, son nom de fichier est mplab_dashboard.png.

On retrouve, dans cette fenêtre, les principales caractéristiques du composant et surtout l’utilisation des mémoires vis à vis du programme.

Le simple programme que nous avons compilé utilise par exemple ici : 10 lignes de code dans la mémoire programme (sur les 2048 disponibles) et 4 cases mémoires de données (sur les 128 disponibles).

Nouvelles variables

On se propose d’ajouter la déclaration suivante :

int b = 0;


Que se passe-t-il alors en mémoire ?

Côté mémoire programme, là où sont stockées les instructions, on voit que 2 lignes de plus ont été utilisées.
Côté mémoire données, ce sont deux cases (octets) supplémentaires qui ont été occupées.

Cela signifie donc que sur ces microcontroleurs, une variable de type int est stockée sur 2 octets (16 bits).
Comme ce microcontroleur gère uniquement des mots de 8 bits en interne, deux lignes de code en assembleur sont alors nécessaires pour initialiser chacun des deux octets de la variable à la valeur 0.

Opérations de base et opérations complexes

On souhaite à présent faire du calcul sur ces variables.
Ajouter tout d’abord la ligne suivante puis compiler le projet.

b = 3;


Cette instruction en C ajoute 4 lignes d’assembleur.

Ajouter à présent cette ligne et compiler le projet.

b = b + a;


Celle-ci en rajoute 6 de plus.

Que se passe-t-il alors si on demande une multiplication ?

b = b * a;


Ce microcontroleur n’est équipé que d’une unité de calcul dite logique. Ainsi certains calculs ne sont pas implémentés structurellement. C’est le cas de l’opération de multiplication. Ce type d’opération est alors réalisée logiciellement. D’autres calculateurs plus performants intègrent ce type d’opérations.

Il en est de même pour la gestion des nombres réels.
Ajouter par exemple la déclaration suivante à votre programme :

double c = 1.0;


Que pouvez-vous conclure du résultat ?

On se propose également maintenant de s’intéresser à des fonctions de maths basiques.
Ajouter la bibliothèque math.h.

#include 


Ajouter la ligne d’instruction suivante :

c = sin(1.0);


Je vous laisse faire les conclusions qui vont bien…

Choix des modules à utiliser

Choix des entrées/sorties

Variables locales vs Variables globales

Dans la plupart des applications en langage C, on préfère déclarer les variables à l’intérieur des diverses fonctions, afin de clarifier le code et le rendre le plus universel possible pour pouvoir réutiliser simplement par la suite nos bibliothèques de fonctions. Il permet également de bien séparer les variables et leurs utilisations respectives dans les fonctions.

double moyenneTab(int tab[], int h, int l){
int i, j;
for(i = ...)
...
}

int main(){
int i, j, k;
double resultat;
...
}


Cependant, cette méthode est très gourmande en ressources matérielles. En effet, à chaque nouvelle variable déclarée dans une fonction, le compilateur lui associera un nouvel espace mémoire (compatible avec le type de variable associé). Or les microcontrôleurs sont des structures de calcul ne bénéficiant pas d’une grande quantité d’espaces mémoires.

Afin de gagner en quantité de mémoires utilisées, on peut alors utiliser des variables dites globales. Celles-ci ne se déclarent qu’à un seul endroit dans le programme et seront alors communes à l’ensemble des fonctions.

int tab[N], i, j, k, h, l;
double resultat;

double moyenneTab(){
for(i = ...)
...
}

int main(){
...
resultat = moyenneTab();
...
}


Le risque est alors de mal les utiliser et d’en réaffecter une à un endroit qui pourrait être critique à l’application embarquée. Il faut donc les manipuler avec beaucoup de précautions.

N.B. Cette dernière remarque sur l’utilisation de variables globales à la place de variables locales tend à disparaître avec l’apparition de microcontrôleurs toujours plus intégrés et donc moins chers, et contenant toujours de plus en plus d’espace mémoire.

Programme embarqué – Scrutation vs Interruption

Les systèmes embarqués sont développés pour réaliser des tâches répétitives et très spécifiques : réguler la température d’une zone, contrôler la vitesse de rotation d’un moteur, piloter un drône afin d’éviter des obstacles…
Ils sont très réactifs aux sollicitations extérieures et ils nécessitent la plupart du temps de pouvoir acquérir des informations de leur environnement pour pouvoir agir en conséquence sur une partie du système.

Pour cela, ils intégrent souvent :

  • divers capteurs (analogiques ou numériques) câblés sur les entrées du microcontroleur qui gère ce système
  • divers actionneurs (moteurs, LED…) câblés sur les sorties du microcontroleur qui gère ce système

Un programme embarqué a donc pour rôle de faire systématiquement :

  1. l’acquisition des grandeurs d’entrée
  2. le traitement et le stockage de ces grandeurs
  3. le calcul des commandes à appliquer sur les sorties
  4. la mise à jour des sorties

Pour faire ceci, il existe deux méthodes que nous allons détailler par la suite.

Programme simple – Par scrutation

La première méthode pour pouvoir réaliser une application embarquée est de scruter les différentes entrées pour ensuite calculer les commandes à appliquer sur les diverses sorties afin de garantir l’exécution de la tâche que doit accomplir le système.

L’attribut alt de cette image est vide, son nom de fichier est scrutation.png.

Un programme pour système embarqué a une structure très particulière. Il doit obligatoirement contenir :

  • Une zone de ressources externes : des bibliothèques de fonctions déjà écrites
  • Une zone de déclaration de variables globales : variables qui permettent de gagner de la place en RAM mais qui sont communes à toutes les fonctions
  • Une zone de déclaration des prototypes de fonctions : fonctions spécifiques à votre projet
  • Une fonction principale, nommée main, contenant :
    • Une zone d’initialisation : déclaration des variables locales, appel aux fonctions d’initialisations des périphériques…
    • Une boucle infinie (while(1)) : particularité des systèmes embarqués, dont le programme ne doit jamais s’arrêter
/* 
* Programme de test / PIC16F150x
* Auteur : J. VILLEMEJANE / Sept 2017 / IOGS
*/

/* Déclaration des Ressources externes */
#include
#include "config.h"

/* Prototypes des fonctions annexes */
void initPIC(void);

/* Déclaration des Variables globales */


/* Fonction principale */
void main(void) {
/* Zone d'initialisation */
initPIC();

/* Boucle infinie */
while(1){
ACTION A REPETER
}
return;
}

/* Définitions des autres fonctions */
/* Fonction d’initialisation des modules du microcontroleur */
void initPIC(void){
// Mode numerique
ANSELA = 0;
ANSELB = 0;
ANSELC = 0;
// LED sur RC0
TRISCbits.TRISC0 = 0;
PORTCbits.RC0 = 1;
// LED sur RC3
TRISCbits.TRISC3 = 0;
PORTCbits.RC3 = 1;

// BOUTON-POUSSOIR
TRISAbits.TRISA2 = 1; // RA2 / IOC -> BP1
TRISAbits.TRISA5 = 1; // RA5 / IOC -> BP2

// FOSC = 16 MHz
OSCCONbits.IRCF = 0b1111;
return;
}


Dans le code précédent, la fonction initPIC() permet d’initialiser le composant, en particulier ses entrées/sorties par rapport à l’application visée. Ici, par exemple, RC0 est une sortie (initialisée à la valeur 1 – le bit 0 du registre PORTC est mis à ‘1’) – le bit 0 du registre TRISC est mis à ‘0’ – et RC3 est une entrée – le bit 3 du registre TRISC est mis à ‘1’. L’ensemble des broches sont configurées en mode numérique – les registres ANSELx sont initialisés à 0.

Programme embarqué – Par interruptions

Afin de pouvoir interagir plus rapidement avec son environnement et prendre en compte des événements extérieurs dès qu’ils arrivent, la plupart des microcontrôleurs sont dotés d’une capacité à interrompre l’exécution d’un programme pour se détourner vers une fonction particulière à exécuter lors de l’arrivée de cet événement extérieur. On appelle cela une interruption.

Pour cela, le microcontrôleur possède plusieurs entrées particulières qui permettent d’interrompre le programme principal. On verra dans des tutoriaux ultérieurs que d’autres modules du microcontrôleur (timers, ADC,…) peuvent également venir interrompre l’exécution du programme principal.

Cette méthode a pour intérêt qu’une sollicitation extérieure est prise en compte uniquement quand elle intervient. Le calcul associé à ce changement est alors réalisé que si une évolution dans les signaux d’entrée intervient.

L’attribut alt de cette image est vide, son nom de fichier est interruption.png.

Cette stratégie est la base des systèmes embarqués dits temps réel. Lors du développement d’une telle application, les concepteurs assurent que toute sollicitation extérieure aura une réponse dans un temps donné et défini à l’avance (en fonction des performances des systèmes matériels choisis).

#include <xc.h>
/* Déclaration des Ressources externes */
#include
#include "config.h"

/* Prototypes des fonctions annexes */
void initPIC(void);
void initINT(void);

/* Déclaration des Variables globales */


/* Fonction principale */
void main(void) {
/* Zone d'initialisation */
initPIC();
initINT();

/* Boucle infinie */
while(1){
ACTION A REPETER
}
return;
}

/* Définitions des autres fonctions */
/* Fonction d’initialisation des modules du microcontroleur */
void initPIC(void){
// Mode numerique
ANSELA = 0;
ANSELB = 0;
ANSELC = 0;
// LED sur RC0
TRISCbits.TRISC0 = 0;
PORTCbits.RC0 = 1;
// LED sur RC3
TRISCbits.TRISC3 = 0;
PORTCbits.RC3 = 1;

// BOUTON-POUSSOIR
TRISAbits.TRISA2 = 1; // RA2 / IOC -> BP1
TRISAbits.TRISA5 = 1; // RA5 / IOC -> BP2

// FOSC = 16 MHz
OSCCONbits.IRCF = 0b1111;
return;
}

/* Fonction d’initialisation des interruptions */
void initINT(void){
IOCAP = 0;
IOCAN = 0;
IOCAPbits.IOCAP2 = 1;
IOCANbits.IOCAN2 = 1;
INTCONbits.IOCIE = 1;

// GLobal
INTCONbits.GIE = 1;
return;
}


/* Routine d'interruption */
void interrupt isr(void){
INTCONbits.GIE = 0;
if(INTCONbits.IOCIF == 1){
if(IOCAFbits.IOCAF2 == 1){ // acquisition
ACTION
IOCAFbits.IOCAF2 = 0;
}
}
INTCONbits.GIE = 1;
return;
}
Microcontrôleurs / Quelle est la structure d’un programme embarqué ?