Bio-Genetic

  • : Embedded
  • embedded
  • : Informatique Linux Logiciel Windows Electronique Hi Tech
  • : [ Embarqué ] Immersion dans les systèmes embarqués dit aussi « enfouis », qui peuvent êtres définis comme des systèmes électronique et informatique autonomes, dédiés à une tâche bien précise. Embarqué est souvent similaire à des contraintes : temps réel, consommation réduite, taille réduite, environnement sévère …. Le terme embarqué désigne aussi bien le matériel que le logiciel qui l’anime.
  • Recommander ce blog
  • Retour à la page d'accueil
  • Contact

Classification

W3C

  • Flux RSS des articles

Programmation

Samedi 18 novembre 2006 6 18 /11 /2006 14:39
Introduction
Souvent mal connu, mal utilisé voir ignoré des programmeurs, l'attribut " volatile " en langage C/C++. Ce n'est pas une surprise, car les ouvrages de programmation et / ou de référence décrivent cet attribut en une ou deux phrases sans vraiment décrire le pourquoi du comment.
Tous les programmeurs d'applications embarquées en langage C / C++ ont déjà eu une expérience plus ou moins malheureuse du type :

- L'application fonctionne parfaitement quand l'optimisation du compilateur est validée.
- L'application fonctionne parfaitement tant que les interruptions sont dé-validées.
- Dans une application multitâches, les tâches fonctionnent bien séparément et l'application se "plante" dès qu'une nouvelle tâche est activée. Si ces mésaventures arrivent cela est dû à la non utilisation de l'attribut volatile.

Volatile est un attribut appliqué à une variable lors de sa déclaration : cela indique au compilateur que le contenu de la variable est susceptible de changer à n'importe quel moment, de manière asynchrone, et force ainsi le compilateur à ne pas réaliser d'optimisation indésirables sur tout le code qui touche de près ou de loin aux variables déclarées avec l'attribut volatile.

Syntaxe
Pour déclarer une variable de type volatile, il faut introduire le mot clé avant ou après le type dans la définition de la variable. Les deux exemples suivant déclarent les variables Var1 et Var2 en int volatile :

volatile int Var1;
int volatile Var2;

Déclaration de pointeurs sur des variables volatiles :

volatile int * Var1;
int volatile * Var2;

Plus rare, la déclaration d'un pointeur volatile sur une variable non volatile : int * volatile Var2;

Déclaration d'un pointeur volatile sur une variable volatile : int volatile * volatile Var2;

Il est préférable de placer l'attribut volatile après le type de la variable, comme dans les deux derniers exemples ; ce n'est pas une règle, c'est avant tout une question de goût.

Pour finir avec la syntaxe, si l'on applique l'attribut volatile à une structure "struct" ou une union, l'intégralité de la structure / union est volatile. Il est bien sur possible de qualifier de volatile chaque membre de la structure ou de l'union, indépendamment des autres.

Utilisation
Une variable doit être déclarée volatile quand son contenu est susceptible de changer de façon aléatoire par rapport au déroulement général du programme. En pratique, il y a seulement trois type de variables qui entrent dans ce cadre :

- Les registres des interfaces de périphériques ( projetés en mémoire ).
- Les variables globales modifiées par une routine d'interruption.
- Les variables globales dans une applications multitâches. Ce type de variables, si elles ne sont déclarées avec l'attribut volatile, vont générées un programme qui ne sera pas fonctionnel : le compilateur vas mal interpréter les actions à réaliser, voir supprimer du code, qui pourtant est nécessaire.

On peut faire comme certains qui déclarent toutes les variables en volatile ou indiquent au compilateur de traiter toutes les variables comme telles : bien que générant un code fonctionnel, cette méthode conduit à un code non-optimisé en terme de place et de performances


Registres des interfaces de périphériques
Les applications embarquées communiquent avec le mode extérieur à travers des périphériques sophistiqués. Les interfaces de périphériques contiennent des registres dont le contenu change de manière asynchrone par rapport au déroulement du programme.
La plupart des compilateurs disposent de l'attribut " @ " qui permet de déclarer une adresse mémoire spécifique ou un registre projeté en mémoire : la déclaration prends la forme : @0x8000;
Bien que fonctionnelle cette déclaration n'est pas conforme au standard C ANSI et n'est pas forcément portable : mieux vaut utiliser une déclaration standard sous forme de pseudo-variables.

Base de travail : ( unsigned char *) 0x8000
Cette déclaration pointe le contenu d'un registre 8 bits à l'adresse 0x8000, mais il n'est pas possible d'écrire ou de lire une donnée ; il faut pour cela dé-référencer le pointeur ( spécifier que la valeur pointée n'est pas le pointeur lui-même ) : * ( unsigned char * ) 0x8000.

La forme définitive et développée, avec l'attribut volatile, prends la forme :
#define PORT_PERIF ( * ( volatile unsigned char * ) ( 0x8000 + OFFSET_REG ) )

Routine d'interruption
Les routines d'interruptions utilisent souvent des variables testées dans le programme principal. Par exemple, l'interruption d'un port série en réception : le programme d'interruption stocke et test les caractères reçus, signalant la fin du message par une variable globale au programme principal :

int End_Message = FALSE;

void main ( void )
{
....
while (! End_Message)

#define RX_CHAR ( * ( volatile unsigned char * ) ( 0x8000 + 0 ) )

interrupt void Isr_Receive ( void )
{
.....
if ( RX_CHAR == ETX ) { End_Message = TRUE;}
.....
.....
}

Si l'optimisation du compilateur est dé-validée, ce programme fonctionne correctement. Par contre n'importe quel compilateur "décent" avec l'optimisation validée, va supprimer le code après la boucle while dans le main(), la condition ( ! End_Message) étant toujours vraie pour lui : en effet le compilateur n'a pas idée que la variable End_Message est modifiée par la routine d'interruption !
Dans ce cas là et pour que tout rentre dans l'ordre il suffit de déclarer la variable End_Message avec l'attribut volatile.

Applications multitâches
Dans les applications multitâches, les tâches communiquent entre elles par mécanismes ( pipes, data box, etc ... ) en utilisant un accès à un emplacement mémoire partagé ( idem variable globale ). La gestion est confié à un gestionnaire de tâches ( scheduler ), dont le fonctionnement dynamique et asynchrone, ne laisse aucune indication au compilateur lors de la génération du code. En d'autres termes, une tâche modifiant une variable globale partagée par une autre tâche ou une autre portion du programme, et on se retrouve dans la même problématique que les interruptions.

On peut donc en tirer une règle de base pour les application multitâches :
Toutes les variables globales partagées doivent être déclarées avec l'attribut volatile.

A lire aussi : optimisation des systèmes embarqués [ épisode 1 ] [ épisode 2 ] [ épisode 3 ] [ épisode 4 ]
Par BigEndian - Publié dans : Programmation
Ecrire un commentaire - Voir les commentaires - Recommander
Samedi 18 novembre 2006 6 18 /11 /2006 14:51
Introduction
Toutes les applications embarquées utilisent les interruptions ; beaucoup supportent le traitement multitâches ou multitread des opérations. Les applications de ce type peuvent s'attendre a ce que le déroulement normal du programme change de contexte, à n'importe quel moment.
Quand une interruption survient, l'opération courante est suspendue et une fonction ou une tâche différente commence à fonctionner. Que se passe-t-il si les fonctions ou les tâches partagent des variables ? ....Un désastre si une fonction corrompt les données d'une autre fonction.

En contrôlant soigneusement comment les données sont partagées, on crée des fonctions réentrantes permettant des appels multiples et concourant qui n'interfèrent pas l'un avec l'autre : cette phrase est la définition même de la réentrance.
Dans les applications embarquées, une fonction ou une tâche est réentrante si elle satisfait aux conditions suivantes :

- Elle emploie toutes les variables partagées de manière "ATOMIQUE", à moins que ces variables soient     assignées spécifiquement à une fonction.
- Elle n'appelle pas de fonctions NON-REENTRANTES.
- Elle n'utilise pas le hard de manière "NON-ATOMIQUE".

Variables atomiques
Le mot "atomique" vient du Grec qui signifie "indivisible". Dans le monde informatique, une opération atomique est une opération qui ne peut être interrompue. Par exemple l'instruction assembleur : mov ax,bx

Puisque rien d'autre qu'un RESET du processeur ne peut stopper ou interrompre cette instruction, celle-ci est atomique : elle débute et se termine sans qu'elle soit perturbée par d'autres tâches ou des interruptions. Autre exemple typique pour gérer des ressources partagées, c'est l'instruction TAS de la famille des processeurs 68000 de Motorola : cette instruction a la particularité de lancer 3 cycles ( lecture - modification - écriture ) de façon indivisible.

La première condition requise pour une fonction réentrante c'est d'utiliser les variables partagées de manière atomique : supposons que deux fonctions partagent une variable globale Toto.

temp = toto;
temp += 1;
toto = temp;

Cette portion de code n'est pas réentrante, parce que toto n'est pas employé de façon atomique : il faut trois actions pour changer sa valeur et non une. La manipulation de la variable toto n'est pas indivisible : une interruption peut survenir entre ces instructions et commuter le contexte à l'autre fonction, qui peut également essayer de changer le contenu de la variable toto : il y a conflit !
Au lieu de cela on peut faire : toto += 1;

Maintenant l'opération est atomique ... ? Une interruption ne peut pas suspendre le traitement avec toto dans un état partiellement changé, ainsi cette routine est réentrante. A moins que ..... le compilateur C produise un code du genre :

mov ax,(toto)
inc ax
mov (toto),ax

Compilé sous cette forme, l'opération qui consiste à incrémenté la variable toto, n'est pas atomique et et n'est pas réentrante non plus. La version atomique doit être sous la forme : inc (toto)

Donc méfiance à l'égard des compilateurs et du code qu'ils sont susceptibles de générer.!!

SI l'aspect atomique est parfois difficilement maîtrisable autre qu'en assembleur, la première règle de la réentrance met en évidence le fait d'utiliser des variables spécifiques ( locales ) pour chaque fonctions : en C et C++, Il suffit d'employer les variables automatiques : c'est à dire les variables qui sont déclarées à l'intérieur d'une fonction et visibles uniquement par celle-ci ; chaque appel de la fonction crée une nouvelle version des variables sur la pile.

Les appels non-réentrants sont prohibés
Cette deuxième règle est évidente et indique qu'une fonction appelante hérite des problème de réentrance de la fonction appelée. En utilisant les langages compilés comme le C et le C++ cela peut générer des problèmes insidieux. Comment être sûr de toutes les fonctions des bibliothèques sont réentrantes ? Évidement, les opération sur les strings et beaucoup d'autres opérations ( calculs, conversion ...) font des appels aux bibliothèques pour effectuer le vrai travail.
Poser la question au fournisseur du compilateur C / C++ pour s'assurer que les fonctions des bibliothèques sont bien réentrantes ; idem pour les packages logiciels tels que les piles de protocoles ( TCP/IP ). En règle générale il faut bien se renseigner avant d'acheter ....

Accès non-atomiques aux interfaces de périphériques

La dernière règle concernant la réentrance s'applique au hard. Dans bon nombre d'architectures, les "entrées / sorties" ou interfaces de périphériques sont "projetés en mémoire", et vus comme de la mémoire ou des variables. Dans le cas ou un accès nécessite plus d'une opération simple pour accéder à l'interface, les problèmes de réentrance peuvent se développer.
C'est le cas par exemple avec certains composants ou contrôleur d'interface, où n'importe quel accès exige deux étapes : on écrit d'abord l'adresse du registre dans un registre d'adresse, puis lit ou on écrit la donnée dans le registre de donnée, à la même adresse.
C'est un cas isolé mais qu'il faut connaître, faute de quoi on retombe dans le même schéma qu'une opération non-atomique.

Quel sont les meilleures options pour produire du code réentrant ?

Le premier principe c'est d'éviter les variables partagées : les variables globales sont souvent une source d'ennuis et de debug fastidieux. Employer les variables automatiques ( locales ) ou l'allocation dynamique de la mémoire. Pourtant les variables globales sont la manière la plus rapide d'échanger des données entre les fonctions ou les tâches : il n'est pas toujours possible de les éliminer entièrement des systèmes en temps réel. Ainsi, en utilisant une ressource partagée ( variable ou matériel ) il faut mettre en oeuvre d'autres techniques.

L'approche la plus simple consiste à neutraliser les interruptions dans la portion du code non-réentrant : avec le rejet des interruptions, le système devient un environnement simple processus. Le masquage des interruptions augmente le temps de latence du système pour la prise en compte des interruptions. Une approche plus élégante consiste à utiliser un "mutex" ( également connu sous le nom de sémaphore binaire ) pour indiquer quand une ressource est occupée. Les sémaphores binaires sont des indicateurs tout ou rien dont le traitement est en soi atomique. Presque tous les systèmes multitâches temps réel dispose d'un service de ce type.

Récursivité
Décrire la réentrance n'est pas complet sans parler de la récursivité, ne serait-ce que il y a souvent confusion entre les deux. Définition de la récursivité : une fonction est récursive si elle s'appelle .
C'est une manière classique d'enlever des itérations dans des algorithmes, bien qu'utilisant beaucoup la pile, et étant moins lisible et plus difficile à vérifier. Puisqu'une fonction récursive s'appelle, elle doit forcément être réentrante pour éviter la destruction de ses variables lors des appels multiples.
Toutes les fonctions récursives doivent être réentrantes, mais toutes les fonctions réentrantes ne sont pas forcément récursives.
Par BigEndian - Publié dans : Programmation
Ecrire un commentaire - Voir les commentaires - Recommander
Samedi 18 novembre 2006 6 18 /11 /2006 15:00
Introduction
Dans la programmation en C d'applications embarquées, il est souvent nécessaire de définir des variables spécifiques à des adresses absolues. Cette définition est utile, par exemple, pour définir des registres d'entrées / sorties ( projetés en mémoire ) pour un circuit d'interface : UART, PIA, contrôleur IDE etc.. ou tout simplement pour les registres internes d'un microcontrôleur. Pour réaliser cette définition il y a trois possibilités :
  • Une MACRO .
  • L'attribut @ .
  • Un segment de données spécifique.

MACRO

L'utilisation d'une MACRO en C permet de définir une variable ou plutôt une pseudo-variable à une adresse absolue :
#define PORT_A ( * ( volatile unsigned char * ) ( 0x1000 ) )

Cette MACRO défini PORT_A comme le contenu d'un pointeur sur un octet à l'adresse absolue 0x1000 ; PORT_A est une pseudo-variable de type "unsigned char" définie à l'adresse 0x1000. L'accès à celle-ci se fait tout naturellement par :

PORT_A = 0x50 /* Ecriture de PORT_A avec la valeur 0x50 */
Temp = PORT_A /* Lecture de PORT_A, resultat dans Temp */

Ce type de définition est PORTABLE sur n'importe quel compilateur ANSI C, mais présente deux inconvénients majeurs :
Il n'y a pas de variable réellement allouée avec cette définition, et par conséquent il n'est pas possible de visualiser ou modifier celle-ci sous un debugger par exemple ( fenêtre mémoire ).
Le Linker n'a aucune idée où les pseudo-variables sont allouées et il ne peut détecter d'éventuelles erreurs ( recouvrement par exemple ).


L'attribut " @ "
Presque tous les compilateurs actuels incluent l'attribut "@" ; celui-ci est utilisé pour spécifier au compilateur de placer la variable ou la constante désignée à une adresse absolue :
volatile unsigned char PORT_A @ 0x1000;

Cette définition n'a pas les inconvénients des pseudo-variables car la variable est effectivement allouée par le compilateur, mais l'attribut @ ne fait pas parti du standard ANSI C : ce type de définition n'est pas forcément portable d'un compilateur à l'autre.


Segment de données spécifique
En utilisant des directives de compilation ( #PRAGMA ) spécifiques, il est facile de définir une ou plusieurs variables à des adresses absolues : il faut d'abord définir les variables dans le segment approprié, puis spécifier l'adresse de début du segment au Linker :

#pragma DATA_SEG PORTA_SEG
volatile unsigned char PORT_A;
#pragma DATA_SEG DEFAULT

Cette définition déclare PORT_A comme une variable de type "unsigned char" dans le segment de données PORTA_SEG. L'adresse de ce segment est ensuite déclarée dans le fichier de configuration du Linker ( édition des liens ) :

SECTIONS
PORTA_SEG = READ_WRITE 0x1000 SIZE 1;

Si cette définition à tous les avantages, elle quand même une contrainte : la définition des segments n'est pas la même d'un compilateur à l'autre. Il faudra donc modifier les sources sur ce point.

Note : concernant la définition des registres, se rapporter aussi à la note technique concernant l'utilisation de l'attribut Volatile .
Par BigEndian - Publié dans : Programmation
Ecrire un commentaire - Voir les 1 commentaires - Recommander
Samedi 18 novembre 2006 6 18 /11 /2006 15:07
Introduction
Dans bon nombre de systèmes à microprocesseur, l'unité centrale lit et écrit des données en mémoire, et communique avec l'extérieur par des messages ayant une structure définie. Le langage C / C++ par exemple, est souvent utilisé pour la programmation système et / ou embarquée et il permet de définir simplement des structures de données ; le problème c'est que souvent les compilateurs / microprocesseurs définissent une même structure de données de façon différentes, causant une incompatibilité dans la définition de l'interface.
Deux raisons à cela :
  • Les restrictions d'alignement.
  • Le modèle mémoire ( big endian / little endian ).


Le modèle mémoire "big endian" ou "little endian"
Le modèle mémoire défini la manière dont les données sont représentées en mémoire. Deux modèles s'affrontent depuis des années : le modèle "big-endian" Motorola et le modèle 'little endian" Intel. Big-endian signifie "big end" ( most significant byte ), indiquant que l'octet de poids fort est en premier ( à l'adresse basse ). Little-endian signifie "little end" ( least significant byte ), indiquant que l'octet de poids faible est en premier ( à l'adresse basse ).

Les microprocesseurs de la famille 80x86 d' Intel et leurs dérivés sont sur le modèle "little endian". Les microprocesseurs SPARC de Sun, les microprocesseurs de la famille 68xxx Motorola et leurs dérivés sont sur le modèle "big endian". Les microprocesseurs PowerPC sont universels, puisqu'il est possible de configurer le microprocesseur en mode "little endian" ou en mode "big endian", ce dernier étant le mode par défaut. On peut noter aussi que JAVA et sa machine virtuelle fonctionnent sur le modèle "big endian", tout comme les couches réseaux des protocoles TCP/IP .... La balance pencherait donc coté "big endian" ?

Exemple d'un mot long ( 32 bits ) 0x0AC0FF00, représenté sur un système "big-endian" et sur un système "little endian".

                         0x20000000 0x20000001 0x20000002 0x20000003
Big-endian     0x0A               0xC0              0xFF               0x00
Little-endian  0x00                0xFF              0xC0               0x0A


Problèmes engendrés par cette différence
D'une manière générale un programme compilé sur une machine "big endian" ne pourra pas fonctionner correctement sur une machine "little endian", et inversement : la représentation et le stockage des mots de 16, 32 et 64 bits est susceptible de poser des problèmes. L'exemple typique est deux machines reliées en réseau via le protocole TCP/IP : ce dernier étant sur le modèle "big endian", une adresse IP par exemple sera envoyée et reçue avec son octet le plus significatif en premier.
Sans traitement particulier, si une des machine ( un PC 80x86 ) envoi une adresse IP 192.0.1.2, qui en "little endian" fait 0x020100C0 ( représentation hexadécimale 32 bits ), à une machine de type SPARC, celle-ci reconstruit une fausse adresse ( 2.1.0.192 ) à partir des octets reçus 0x02,0x01,0x00 et 0xC0. D'où la nécessité d'identifier le type de modèle mémoire et de réaliser ou non les traitements particuliers pour stocker correctement les données en mémoire.


Pourquoi des modèles mémoires différents ?
Il n'y a pas de raisons logiques et valables pour que les fabricants de microprocesseurs décident d'utiliser des modèles mémoires différents. La raison est principalement historique : elle est née dans le combat qui à toujours opposé les deux géant du semi-conducteur, Motorola ( big-endian ) et Intel ( little-endian ). Malgré tout la balance penche plutôt du coté "big endian" notement en ce qui concerne les applications réseaux ( routeur, passerelle réseau, connectivité TCP/IP ... ) : le fonctionnement de telles applications sera pénalisé en durée d'exécution sur une machine "little endian", par la nécessité de convertir de façon bidirectionnelle les mots de 16, 32 et 64 bits ( adresses IP, checksum ... ).


Détection du type de modèle mémoire sur une machine en langage C.
Ci-dessous un exemple d'une routine permettant de détecter le modèle mémoire utilisé par le microprocesseur de la machine sur laquelle elle est exécutée.
int Little_Endian (void)
{
   int x = 1;
   return ( x == *((char *)&x)); /* Retourne 1 si Little endian, si différent de 1 c'est Big endian */
}


Conversion big-endian<>little-endian en langage C
Ci-dessous un exemple de deux routines de conversion big-endian > little-endian et inversement, permettant la conversion d'un mot et d'un mot long.
short convert_short (short in)
{
    short out;
    char *p_in = (char *) &in;
    char *p_out = (char *) &out;
    p_out[0] = p_in[1];
    p_out[1] = p_in[0];
    return out;
}

long convert_long (long in)

{
   long out;
  char *p_in = (char *) &in;
  char *p_out = (char *) &out;
  p_out[0] = p_in[3];
  p_out[1] = p_in[2];
  p_out[2] = p_in[1];
  p_out[3] = p_in[0];
  return out;
}
Par BigEndian - Publié dans : Programmation
Ecrire un commentaire - Voir les 1 commentaires - Recommander
Samedi 18 novembre 2006 6 18 /11 /2006 15:16
Introduction
Dans bon nombre de systèmes à microprocesseur, l'unité centrale lit et écrit des données en mémoire, et communique avec l'extérieur par des messages ayant une structure définie. Le langage C / C++ par exemple, est souvent utilisé pour la programmation système et / ou embarquée et il permet de définir simplement des structures de données ; le problème c'est que souvent les compilateurs / microprocesseurs définissent une même structure de données de façon différentes, causant une incompatibilité dans la définition de l'interface.
Deux raisons à cela :
  • Les restrictions d'alignement.
  • Le modèle mémoire ( big endian / little endian ).

Restrictions d'alignement

Les microprocesseurs et microcontrôleurs 16, 32 ou 64 bits ne peuvent pas accéder à des mots ( 16 bits ) ou des mots long ( 32 bits ) à n'importe quelle adresse. Par exemple le microprocesseur Motorola MC68000 ne permet des accès 16 bits qu'à une adresse paire ( even address ) ; un accès 16 ou 32 bits à une adresse impaire provoque une exception de type "erreur adresse".

Pourquoi une restriction de l'alignement ?
Suivant la taille réelle du bus de données, le microprocesseur accède à la mémoire en réalisant un cycle bus sur 16, 32 ou 64 bits ; dans le cas d'un processeur 32 bits ( exemple ci dessous ), le cycle bus est réalisé à des adresses multiples de 4, et donc le microprocesseur n'utilise pas les lignes d'adresses A1 et A0 pour adresser la mémoire.
Les raisons qui interdisent un accès non aligné ne sont pas difficiles à voir. Par exemple un mot long X aligné sur une adresse paire est écrit avec X1,X2,X3 et X4 : il peut être alors lu en un seul cycle bus. Dans le cas ou le microprocesseur tente une lecture d'un mot long à partir d'une adresse impaire, il va lire aussi Y1,Y2,Y3 et Y4, mais dans ce cas la lecture ne pourra se faire en un seul cycle bus : c'est l'exemple type d'un accès non aligné. La plupart des microprocesseurs ne tolèrent pas des accès non- alignés, cela provoquant dans la majorité des cas une erreur / exception prioritaire.
                           Octet 0   Octet 1   Octet 2   Octet 3
0x20000000
0x20000004        X0           X1           X2            X3
0x20000008
0x2000000C                        Y0            Y1            Y2
0x20000010        Y3


Alignement des données par les compilateurs
Les compilateurs appliquent les restrictions d'alignement définies par les microprocesseurs cibles pour lesquels ils sont destinés ; c'est ainsi que les compilateurs ajoutent des octets "d'alignement" dans les structures de données, les variables et le code machine du programme compilé, afin de ne pas violer les restrictions d'alignement définies par le microprocesseur cible.
Le travail du compilateur est illustré dans l'exemple ci-dessous, avec char = 1 octet, short = 2 octets, et long = 4 octets, sur un microprocesseur 32 bits :
Programme source utilisateur Programme source après compilation
struct Message
{
    short entete;
    char commande;
    long taille_message;
    char version;
    short destination;
};

struct Message
{
    short entete;
    char commande;
    char align01; // debut de structure alignée sur un mot long
    long taille_message;
    char version;
    char align02; // version aligné sur un mot
    short destination;
    char align03[4]; // alignement de toute la structure sur 16 octets
};

Dans l'exemple ci-dessus le compilateur ajoute des octets pour respecter les règles d'alignement en vigueur sur le microprocesseur cible. Si cette structure de données est utilisée sur différentes combinaisons de compilateurs / microprocesseurs, les octets ajoutés vont être différents : deux applications utilisant la même structure de données avec un processeur différent, ne vont pas être compatibles.
Une bonne pratique consiste a aligner les types par paires et / ou insérer explicitement des octets d'alignement dans la déclaration des variables. Exemples :

La déclaration suivante n'est pas optimisée : en effet le compilateur va rajouter 4 octets d'alignement, l'ensemble occupant alors 12 octets.

char a;
short b;
char c;
int c;

La même déclaration optimisée "à la main" n'occupe que 8 octets, le compilateur n'ayant pas eu recours à un alignement implicite :

char a;
char c;
short b;
int c;

Règles d'alignement générales
Les règles d'alignement ci-dessous sont généralement utilisées sur les microprocesseur 32 bits. Il faut consulter le manuel de programmation du compilateur et du microprocesseur cible pour obtenir les renseignements sur les règles d'alignement en vigueur.
  • Un seul octet ( char ) est aligné à toutes les adresses ( paires et impaires ).
  • Deux octets ( short ) sont alignés sur un multiple de 2, adresses paires.
  • Quatre octets ( long ) sont alignés sur un multiple de 4, adresses paires.
  • Une structure comprise entre 1 et 4 octets est redimensionnée sur 4 octets.
  • Une structure comprise entre 5 et 8 octets est redimensionnée sur 8 octets.
  • Une structure comprise entre 9 et 16 octets est redimensionnée sur 16 octets.
  • Une structure supérieure à 16 octets est alignée sur un multiple de 16.

D'une manière générale et pour augmenter les performances il convient d'aligner explicitement les structure de données, et de faire en sorte que la taille de la structure soit un multiple de 2 : l'accès aux éléments de la structure n'en sera que plus rapide ( indexation).

Variables locales
Les règles d'alignement sont aussi valables pour les variables locales ( allouées sur la pile ). Généralement, pour les microprocesseur 32 bits, les variables locales sont alignées sur 16 ou 32 bits ; si la déclaration d'une variable de type char est bien sûr possible, celle-ci sera automatiquement alignée sur 16 bits.
char ( accès non aligné ) : alignement sur 16 bits.
short : alignement sur 16 bits.
int, long, float : alignement sur 32 bits.
Par BigEndian - Publié dans : Programmation
Ecrire un commentaire - Voir les commentaires - Recommander
Lundi 10 mars 2008 1 10 /03 /2008 14:04
Le mode Thumb est un mode indépendant et un second jeu d’instruction greffé sur le jeu d'instructions ARM RISC standard. Il est possible d’alterner entre les deux séries d'instructions par l'intermédiaire d'un commutateur de mode dans le code. Le cœur du processeur exécute toujours des instructrions ARM 32 bits : un décompresseur matériel convertit les instructions Thumb vers leur équivalent en instruction ARM ; la décompression matérielle s’effectue à l’étage du décodage du pipeline. Le mode « Thumb Instruction Set Architecture » ( ISA ) est composé d’environ 36 instructions sur 16 bits, permettant de faire des opérations de base telles que : addition, soustraction, branchements, et les opérations de rotation. En utilisant ces instructions à la place des instructions ARM normales sur 32 bits, il est possible de réduire la taille du code de 20 à 30 %.

Il y a plusieurs méthodes pour passer du mode ARM à un mode thumb et vice versa. La méthode la plus usuelle est d’utiliser l’instruction de branchement spéciale BX (Branch and eXchange).
Etant donné que les instructions ARM et Thumb sont alignés sur des frontières de 16 ou 32 bits, le bit 0 du PC n’est jamais utilisé par les instructions de branchements standards. Ainsi, l’instruction BX se branchera à l’adresse spécifiée dans le registre argument Rm en passant en mode Thumb si le bit Rm[0] est égal à 1, en mode ARM sinon. D’autre part il est possible de retourner en mode ARM sur une Exception, une interruption matérielle ou une interruption logicielle ( Software Interrupt ).

Label_ARM_code:
      mov r3, label_Thumb_code
      or r3, r3, #1
      bx r3

Label_Thumb_code:
       ……..
      retour en mode ARM identique en mettant le bit 0 à 0 ....

En langage évolué une simple directive de compilation sera suffisante ( exemple ) :

#Pragma __ARM__

#Pragma __THUMB__

Si l’instruction BX Rm est exécutée, alors le processeur se branche a l’adresse specifiee par Rm[31..1], et fait le changement de mode:
_ Si Rm[0]=1⇒mode Thumb
_ Si Rm[0]=0 ⇒mode ARM


Il n’est pas possible de mixer n’importe comment du code Thumb et du code ARM ;  il faut expressément switcher d’un mode à l’autre, et donc de séparer les blocs de code 16 bits Thumb du code ARM 32 bits. D’autre part le mode Thumb est abrégé et simplifié par rapport au mode ARM, et il n’est pas possible de faire certaines opérations : gérer les interruptions, les exceptions, les sauts longs, les transactions atomiques en mémoire ou des opérations coprocesseur. De petites séquences d’instructions ARM deviennent des séquences plus longues si elles sont traduites en instructions Thumb.  Une autre limitation c’est les registres disponibles : en mode Thumb plus que 8 registres au lieu de 16 ; le passage de paramètres entre mode ARM et mode Thumb code n'est pas difficile, tant qu’ils sont passés par la pile ou par le biais du processeur et des huit premiers registres. Passer et sortir du mode Thumb prend aussi du temps et, paradoxalement, ajoute du code ( c’est un comble ) : quelques dizaines d'octets de préambule et postambule sont nécessaires pour organiser des pointeurs et vider le cache CPU.

Que faire alors du mode Thumb ? l’utiliser quand le budget de taille du code / taille mémoire est serré et / ou pour du code non critique. D’autre part il ne faut pas utiliser le mode Thumb pour quelques instructions mais une dizaine au minimum. Les performances sont globalement affectées, d’environ 15 % par rapport au mode ARM, pénalisées par la commutation ARM / thumb qu’il faut vraiment utiliser avec parcimonie. Un point positif sur la gestion du cache plus efficace du fait des instructions sur 16 bits….
Par BigEndian - Publié dans : Programmation
Ecrire un commentaire - Voir les commentaires - Recommander

Atomic Clock

Décembre 2009
L M M J V S D
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
<< < > >>

Yahoo Search

Blog Policy

Blog non commercial régulièrement mis à jour. Les marques et liens cités le sont au seul titre d'information et de commodité : aucune rémunération n'est perçue pour cette publicité indirecte.

Tout internaute a la possibilité de publier des commentaires, sous sa responsabilité. Cette publication est libre dans la mesure où cela est pertinent par rapport à l’article considéré.
Les contenus des liens et commentaires textuels ne devront pas être contraires aux bonnes mœurs, à l’ordre public, ni aux lois et réglementation en vigueur. Ils devront être libres de tous droits et seront sous l’entière responsabilité de leur auteur.

Sont proscrits les comportements tels que le détournement de service à des fins commerciales ou professionnelles, la contrefaçon de marques déposées, la divulgation d’informations nominatives sur les personnes, la violence ou l’incitation à la violence politique, raciste ou xénophobe, la pornographie, la pédophilie, le révisionnisme, le négationnisme, les commentaires à caractère diffamatoire ou injurieux, les discriminations de toutes natures, et toutes les activités illégales de copies d’œuvres telles que logiciels, photos et images. [ BigEndian ]
Créer un blog sur over-blog.com - Contact - C.G.U. - Rémunération en droits d'auteur - Signaler un abus