Les micro-contrôleurs dans les systèmes embarqués

Elham Firouzi

Ce cours traite les thèmes suivants :

- systèmes embarqués ;

- méthodes de design ;

- sécurité et fiabilité des systèmes à microprocesseur ;

- connexions aux composants périphériques ;

- communication avec les autres systèmes à microprocesseur.

N'hésitez pas à commenter cet article ! Commentez

- Titre : Les micro-contrôleurs dans les systèmes embarqués

- Auteur : Elham Firouzi

- Parution : 1 janvier 2011

- Mise à jour : 30 mars 2014

- Licence : Copyright ® 2011 Elham Firouzi. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Structure du cours

- I-B. Domaine d'application des microcontrôleurs

- I-C. Les caractéristiques des microcontrôleurs

- I-D. Les langages de programmation

- I-D-1. C

- I-D-2. C++

- I-D-3. Assembleur

- I-D-4. Java

- I-E. Environnements et outils de développement

- I-E-1. Simulateur

- I-E-2. Moniteur ROM

- I-E-3. Interface JTAG

- I-E-4. Émulateur

- I-E-5. Programmation et test direct de ROM / Flash

- II. Généralités

- II-A. Les systèmes embarqués

- II-B. Les systèmes temps réel

- II-C. Le processus technique

- II-D. Les systèmes de communication industriels

- II-D-1. La structures des systèmes de production industriels

- II-D-2. Les critères pour la communication

- III. Méthodes de conception

- III-A. Déroulement cyclique

- III-B. Déroulement quasi parallèle et événementiel

- III-C. Déroulement quasi parallèle avec RTOS

- III-D. State-Event

- III-D-1. Introduction

- III-D-2. Conception

- III-D-2-a. Tableau d'états

- III-D-2-b. Diagramme d'état

- III-D-3. Implémentation de diagrammes d'état

- III-D-3-a. Instructions switch emboîtées

- III-D-3-b. Les tableaux

- IV. Sécurité des systèmes

- IV-A. Introduction

- IV-A-1. Hardware

- IV-A-2. Software

- IV-B. Le langage de programmation C

- IV-B-1. MISRA-C

- IV-B-2. Analyse de code statique

- IV-C. Murphy et le langage de programmation C

- IV-C-1. Dépassement de capacité dans les tableaux

- IV-C-2. Dépassement de capacité dans la pile (anglais : stack)

- IV-C-3. Pointeur NULL

- IV-C-4. Les interruptions

- IV-C-5. Allocation de mémoire dynamique

- IV-C-6. Les conditions de course

- IV-C-7. Code réentrant

- IV-C-8. Programmation défensive

- IV-C-9. assert()

- IV-C-10. Optimisation du code C

- IV-D. Sécurité au niveau du système

- IV-D-1. La conception

- IV-D-2. Spécification et exécution des tests

- IV-D-3. La documentation

- IV-D-4. Reviews

- IV-E. Watchdog

- IV-E-1. Introduction

- IV-E-2. Le watchdog externe

- IV-E-3. Le watchdog interne

- IV-E-4. Fonctionnement du watchdog

- IV-E-5. Démarrage du watchdog

- IV-E-6. Rafraîchissement du watchdog

- V. Connexion de la périphérie

- V-A. Bus d'adresse / de données

- V-B. Port d'entrée et de sortie numérique

- V-C. RS232

- V-D. SPI

- V-E. I2C

- V-E-1. Aperçu

- V-E-2. Protocole

- V-E-3. Implémentation

- V-F. Timer

- V-F-1. Introduction

- V-F-2. Timer/Counter

- V-F-3. Timer avec recharge automatique

- V-F-4. Unité comparatrice

- V-F-5. Entité de capture

- V-G. CAN

- VI. Références

- VII. Remerciements Developpez

I - Introduction

I-A - Structure du cours

Le premier chapitre de ce cours traite les domaines d'application des microcontrôleurs embarqués, leurs caractéristiques typiques et les outils de développement des logiciels. Le second chapitre fournit d'importantes notions de base, qui sont essentielles à la compréhension de ce module. Le chapitre trois traite un des aspects les plus importants du développement de logiciels embarqués : la conception (design). Le quatrième chapitre se concentre sur la sécurité et la fiabilité des systèmes à microprocesseur. En effet, les erreurs logicielles dans les systèmes embarqués peuvent générer de sérieux problèmes. Le cinquième chapitre traite de la communication dans son ensemble. Il montre, d'une part, comment les composants périphériques peuvent être connectés à un microcontrôleur embarqué et, d'autre part, comment les systèmes peuvent communiquer les uns avec les autres.

Les deux thèmes suivants sont explicitement exclus de ce cours bien que faisant partie du domaine de l'embarqué : RTOS (Real-Time Operating System ou Système d'exploitation temps réel) et le serveur Web intégré (connexion Internet de l'équipement et la machinerie).

I-B - Domaine d'application des microcontrôleurs

Le choix du microcontrôleur 8 bits, 16 bits ou 32 bits dépend de la complexité de l'application embarquée. La notion de systèmes embarqués est traitée plus en détail au chapitre II. Ces derniers sont essentiellement constitués d'une partie matérielle (hardware) et d'une partie logicielle (software). Leurs domaines d'applications sont les suivants :

Système de communication :
Les microcontrôleurs 8 bits sont souvent utilisés pour les téléphones portables simples et la téléphonie fixe alors que les microcontrôleurs 32 bits se retrouvent plutôt dans les smartphones et les PDA.
Les processeurs 8 bits ou 32 bits sont utilisés pour le raccordement des capteurs et actionneurs aux systèmes de bus en fonction de la complexité du bus et de l'application.

Technique médicinale :
Les instruments de mesure (par exemple mesure de la glycémie), les organes artificiels, etc. Selon la complexité de l'application, microcontrôleurs 8 bits, 16 bits ou 32 bits.

Les technologies de la sécurité :
Les systèmes pour gérer la sécurité dans les moyens de transport (par exemple : les passages à niveau), dans les bâtiments (par exemple : alarme incendie, effractions), etc.
Les microcontrôleurs 8 bits sont utilisés en particulier dans les appareils périphériques alors que les microcontrôleurs 32 bits assument les tâches de contrôle et de gestion.

Mécatronique et automation industrielle :
Installation pour la production de biens, pour la logistique, etc. Les microcontrôleurs 8 bits sont utilisés en particulier pour les capteurs et actionneurs alors que les microcontrôleurs 32 bits assument les tâches de contrôle et de gestion.

Moyens de transport :
Autos, avions, vélo électrique, etc.

Électronique de consommation :
Appareil Hifi, TV, vidéo, beamer, télécommande, etc.

Application basse consommation :
Les appareils à piles, tels que ceux développés par la HESB pour la station ornithologique suisse, qui permettent d'enregistrer des données spatiales et télémétriques. Le choix des microcontrôleurs se limite principalement à 8 bits en raison de la faible consommation d'énergie et de la petite taille. Toutefois, ce dernier peut être remplacé par un microcontrôleur à 16 voire 32 bits lorsque la puissance de calcul n'est pas suffisante.

I-C - Les caractéristiques des microcontrôleurs

La tendance actuelle est de s'éloigner de plus en plus des microcontrôleurs 8 bits afin de s'approcher des microcontrôleurs 32 bits. Les processeurs 8 bits sont utilisés dans les applications qui ont pour critères principaux la consommation et le coût. Les processeurs 16 bits sont relativement peu répandus. Il existe néanmoins quelques exemples comme le MSP430 de TI. Le prix des processeurs 32 bits actuels, comme le Cortex-M3, est devenu tellement intéressant que ces derniers remplacent de plus en plus les processeurs 8 bits.

Les processeurs 32 bits les moins chers coûtent environ un euro.

Les microcontrôleurs 8 bits présentent les avantages suivants faces aux microcontrôleurs 32 bits :

- faible consommation ;

- bas coût ;

- dimension réduite.

Les microcontrôleurs 32 bits présentent les avantages suivants faces aux microcontrôleurs 8 bits :

- puissance et vitesse de calcul supérieures ;

- convient pour l'utilisation d'un système d'exploitation. En effet les performances de la CPU et la taille de la mémoire sont suffisantes ;

- espace d'adressage plus grand.

Taille de mémoire requise pour les microcontrôleurs dans les systèmes embarqués :

- la taille de la mémoire programme s'étend typiquement de quelques kilooctets à quelques centaines de kilooctets. Cette règle est valable pour les microcontrôleurs 8 et 32 bits ;

- la taille de la mémoire pour les données s'étend de quelques centaines d'octets à quelques centaines de kilooctets ;

- Le portage d'un système d'exploitation embarqué comme celui de Linux ou de Windows CE augmente considérablement l'exigence concernant la mémoire programme ou de données à quelques mégaoctets.

I-D - Les langages de programmation

I-D-1 - C

Le langage de programmation C est la référence pour programmer les microcontrôleurs avec des exigences temps réel (voir chapitre II). En effet, ce dernier présente l'avantage d'être un langage de programmation orienté matériel. Le code machine généré par le compilateur est presque optimal en ce qui concerne d'une part la taille de la mémoire requise et d'autre part la vitesse d'exécution du programme. Par conséquent, environ 65 % des applications embarquées sont programmées en C. Toutefois, le langage C présente également des inconvénients qui seront traités dans la section IV.C de ce cours.

I-D-2 - C++

C ++ nécessite plus de ressources (utilisation de la mémoire, la puissance du processeur) que C (environ 20 %).

Cela dépend essentiellement de comment le programme a été défini en C++. Est-ce que ce dernier utilise le polymorphisme ou non ? Le « C++ embarqué » est une version plus dépouillée du C++.

C++ présente les avantages de la programmation orientée objet, ce qui est particulièrement intéressant pour les applications plus complexes. Environ 25 % des applications embarquées sont programmées en C++.

I-D-3 - Assembleur

Les applications programmées uniquement en assembleur sont relativement rares. Par contre, l'assembleur est souvent combiné avec du C ou du C++. Ce langage est principalement utilisé pour optimiser individuellement les fonctions. Par exemple pour les drivers (accès direct au hardware) ou les algorithmes qui nécessitent des calculs intensifs. Moins de 10 % de toutes les applications embarquées sont programmées (du moins partiellement) en assembleur.

I-D-4 - Java

Java est aujourd'hui rarement utilisé pour programmer les applications embarquées (moins de 3 %). Java n'est pas adéquat pour les applications temps réels. En effet, ce langage est interprété (lent) et le comportement temporel du « récupérateur de place » (anglais : Garbage Collector) n'est pas reproductible.

I-E - Environnements et outils de développement

Les outils CASE, les éditeurs, les compilateurs/compilateurs croisés (anglais : cross compiler) et les relieurs (anglais : linker) sont utilisés pour développer les logiciels. Cette section s'intéresse en particulier aux différentes possibilités de débogage qui existent dans le domaine embarqué. Les variantes suivantes sont mises à disposition en fonction des environnements de développement et des types de microcontrôleurs :

1. Simulator.

2. ROM-Monitor.

3. Background Debug Mode.

4. Émulateur.

5. Programmation et test direct sur ROM / Flash.

I-E-1 - Simulateur

Le simulateur permet de tester le code sans que la partie matérielle ne soit disponible. Le comportement du microcontrôleur et des composants de stockage RAM / ROM est simulé sur le PC. La plupart des simulateurs permettent également de commander le comportement des pins d'entrée et de sortie de façon limitée.

Les simulateurs sont souvent utilisés dans la phase de démarrage du développement, jusqu'à ce que les prototypes de la partie matérielle soient disponibles. Ces derniers permettent également de tester les performances des algorithmes.

I-E-2 - Moniteur ROM

Le moniteur ROM (anglais : ROM-Monitor) est la méthode plus simple pour tester une application embarquée.

Un programme moniteur (anglais : Rom-Monitor) est exécuté pour cela sur la partie matérielle. Ce dernier communique avec l'environnement de développement et assure les tâches suivantes :

- téléchargement du code à partir de l'environnement de développement dans la RAM ;

- insertion des points d'arrêts (anglais : break points) ;

- exécution du code pas à pas ;

- affichage du contenu des variables.

Le désavantage du moniteur ROM réside dans le temps de calcul de la CPU et les ressources matérielles supplémentaires (mémoire, interface sérielle), qui sont nécessaires à l'exécution de ce dernier. Toutefois, le moniteur ROM est souvent utilisé à cause de son prix.

I-E-3 - Interface JTAG

Certains microcontrôleurs possèdent une interface JTAG intégrée pour déboguer (ARM par exemple / XScale).

La communication entre le PC et la platine de test est assurée par un wiggler. Ce dernier est connecté d'une part au port USB du PC (ou une autre interface comme RS232) et d'autre part à l'interface JTAG du microcontrôleur.

Le débogage avec l'interface JTAG ne nécessite pas de puissance de calcul supplémentaire. Ce qui constitue incontestablement un avantage. Néanmoins, les désavantages de cette interface sont d'une part l'augmentation du prix du microcontrôleur (plus de silicium) et d'autre part la nécessité de travailler avec un wiggler. En ce qui concerne les fonctionnalités mises à disposition, l'interface JTAG se comporte comme le moniteur ROM.

I-E-4 - Émulateur

Le microcontrôleur est remplacé par un émulateur, qui permet de simuler ce dernier. L'émulateur est souvent réalisé avec des versions spéciales des microcontrôleurs, appelées puces bond out. Les émulateurs sont chers mais ils permettent de réaliser un débogage temps réel sans restriction.

I-E-5 - Programmation et test direct de ROM / Flash

Les ingénieurs en génie logiciel qui n'ont pas de ressources pour l'installation d'un environnement de test sont à plaindre. Ces derniers n'ont pas d'autre choix que de télécharger et de tester le code à partir de la Flash. Les tests sont alors effectués avec la fonction printf() afin d'afficher le comportement de l'application sur un terminal.

Les tests effectués de cette manière nécessitent en général beaucoup plus de temps. Par conséquent, même dans de petits projets, un environnement de développement avec un moniteur ROM ou une interface sont rapidement rentabilisés.

II - Généralités

Ce chapitre introduit des notions de base pour les systèmes embarqués.

II-A - Les systèmes embarqués

La littérature fournit pour la notion de « système embarqué » les définitions suivantes :

Laplante :

- « A software system that is completely encapsulated. »

Douglass :

- « … the computational system exists inside a larger system to achieve its overall responsibility »

Les systèmes embarqués (en anglais : embedded systems) sont composés d'une partie matérielle (hardware) et d'une partie logicielle (software). Ils sont intégrés ou embarqués dans un produit.

Ces produits sont par exemple un robot, une auto, un téléphone portable, un agenda électronique, une machine à café, etc. Le système embarqué est en général responsable du contrôle, du traitement du signal et de la surveillance des différents composants du produit ou du produit dans son ensemble. La fonctionnalité d'un système embarqué se limite à l'exécution d'une ou quelques tâches dédiées.

II-B - Les systèmes temps réel

La littérature fournit pour la notion de « système temps réel » la définition suivante :

Laplante :

- « A real-time system is a system that must satisfy explicit (bounded) response-time constraints or risk severe consequences, including failures. A failed system is a system which cannot satisfy one or more of the requirements laid out in the formal system specification. »

Un système temps réel doit toujours livrer des réponses correctes dans des délais prédéfinis. Le dépassement de ces délais se traduit par un dysfonctionnement du système. Par conséquent, dans un système temps réel, non seulement le résultat mais aussi l'instant auquel ce dernier est livré sont déterminants. Remarque : la définition n'aborde pas le délai en soi. Selon les types de spécification, ce dernier peut correspondre à quelques microsecondes, millisecondes voire secondes. Il est seulement important que le résultat soit fourni à la limite de temps définie !

Contraintes temps réel mou/dur

Dans les systèmes temps réel, on distingue souvent entre les contraintes temps réel mou et temps réel dur.

On parle de contrainte temps réel mou (souple) lorsque le système respecte souvent la spécification temporelle mais pas toujours. Dans ce cas le comportement temporel n'est pas toujours prévisible. Les conséquences de la non-conformité sont ennuyeuses mais pas graves. Exemple : la diffusion d'un film sur un appareil portatif qui s'effectue de façon « saccadée ».

On parle de contrainte temps réel dur lorsque le système respecte toujours les spécifications temporelles. Le système présente alors un comportement temporel déterministe. L'automation industrielle et les applications liées à la sécurité constituent les domaines typiques pour les systèmes temps réel dur (ex. airbag).

Réalisation des contraintes temps réel

Les conditions suivantes doivent être remplies afin de pouvoir respecter les temps de réponse requis :

1. Un design épuré et réfléchi qui prend en compte les exigences temps réel.

2. Des basses latences d'interruption. Cela nécessite que :

1. Les interruptions ne doivent être désactivées que très brièvement.

2. Les routines de service d'interruption doivent être aussi courtes que possible.

3. Un code compact et rapide. Cela affecte également le choix du langage de programmation (C-Code, également C++ en fonction de la CPU, éventuelles optimisations des points critiques en assembleur).

4. Des tests approfondis qui tiennent compte de toutes les situations et conditions possibles. La capacité temps réel est très difficile à démontrer !

II-C - Le processus technique

Ce terme apparaît principalement dans le cadre d'automation industrielle. Voici une brève synthèse :

Définitions du « processus » selon DIN 6620 :
Un processus est une transformation/transport de matière/énergie/information.
L'état du processus technique peut être mesuré, contrôlé et géré par des moyens techniques.

La figure suivante montre l'interface entre un système embarqué et un processus technique. Selon les applications, le système embarqué peut être équipé d'une interface utilisateur ou d'une connexion à d'autres systèmes :

Figure 1 : Interface entre l'ordinateur temps réel et le processus technique.

Le processus technique est la source et la destination des données techniques. Le système embarqué lit l'état du processus à l'aide des capteurs et il influence cet état à l'aide des actionneurs, du point de vue du système embarqué.

Exemples de capteur :

- interrupteur mécanique, inductif ou optique ;

- convertisseur A/D ;

- sonde de température, sonde de pression.

Exemples d'actionneur :

- soupape ;

- moteur ;

- écran, LED ;

- relais ;

- convertisseur D/A ;

La figure suivante montre en détail les transferts de données entre l'ordinateur temps réel et le processus technique.

Figure 2 : Transfert de données entre un ordinateur temps réel et le processus technique.

Le processus technique possède des entrées et des sorties, qui peuvent être soit de l'information, soit du matériel.

Les contre-réactions ou les valeurs de dérangement influencent également le processus technique. Des paramètres caractéristiques peuvent être transmis au processus technique.

L'ordinateur temps réel analyse le processus technique en lisant les valeurs de mesure à des instants précis, qui sont définis par les occurrences des événements. Les algorithmes de contrôle permettent alors de définir des valeurs de contrôle afin d'assurer le fonctionnement correct du processus technique.

II-D - Les systèmes de communication industriels

II-D-1 - La structures des systèmes de production industriels

Des systèmes industriels sont souvent organisés de manière hiérarchique. Dans la figure suivante, nous trouvons un exemple simple et très général :

Figure 3 : La hiérarchie dans un système industriel

Un système industriel consiste en plusieurs calculateurs et/ou systèmes imbriqués sur différents niveaux hiérarchiques. De plus, il y a un ou plusieurs systèmes de communication pour échanger les données.

Le système représenté à la Figure 3 a été partagé en trois niveaux, qui possèdent les tâches suivantes :

Niveau de commande :

- Ce niveau est responsable de l'analyse des données systèmes (coordination et planification), qui est essentiellement réalisée avec des microprocesseurs 32 et 64 bits.

Niveau de contrôle :

- Les différents processus techniques sont contrôlés ou gérés (système asservi) à ce niveau. On se sert de microcontrôleurs 32 bits ou de microprocesseurs.

Niveau processus :

- Sur ce niveau, on trouve les capteurs qui lisent les données processus, ainsi que les acteurs qui influencent directement le processus. Des microcontrôleurs 8 bits sont utilisés principalement à ce niveau. Toutefois, des microcontrôleurs 32 bits sont de plus en plus utilisés à ce niveau. En effet les prix de ce type de processeur ont fortement diminué alors que les exigences pour la communication entre les différents nœuds ont augmenté.

Suivant la complexité du système, on a également plus de trois niveaux. Souvent ce sont :

- niveau de contrôle d'entreprise ;

- niveau de finition ;

- niveau d'installation ;

- niveau cellulaire ;

- niveau de champ ;

- niveau processus.

II-D-2 - Les critères pour la communication

Les critères pour la transmission de l‘information dans un système de production industriel, comme celui décrit au chapitre II.D.1 peuvent varier considérablement. En effet ces derniers dépendent d'une part de l'application et d'autre part du niveau hiérarchique. Les principaux critères sont les suivants :

- capacité temps réel pour les niveaux processus et contrôle ;

- taux de transfert ;

- standards industriels / degré d'utilisation ;

- fonctionnalité ;

- tolérance erreurs / fiabilité / immunité contre des perturbations ;

- économie (optimisation des coûts).

Les deux critères les plus importants sont la capacité temps réel et le taux de transfert. Ces derniers sont illustrés graphiquement à l'aide de pyramide de communication :

Figure 4 : La pyramide de communication.

Le chapitre V aborde certains systèmes de communication plus en détail.

III - Méthodes de conception

La fonctionnalité d'un système peut toujours être décomposée en sous-fonctions (modularisation). Il existe plusieurs stratégies pour exécuter ces sous-fonctions (ex. : déroulement cyclique, RTOS, etc.). Les différentes approches sont discutées dans les sections suivantes. Il est crucial d'analyser le déroulement du programme durant la phase de conception afin de pouvoir définir une structure adéquate pour ce dernier.

III-A - Déroulement cyclique

Toutes les fonctions du programme sont exécutées l'une après l'autre dans l'approche cyclique. Lorsque la dernière fonction du cycle a été exécutée, le programme continue avec l'exécution de la première fonction de ce dernier. L'intervalle de temps nécessaire pour exécuter un cycle entier dépend essentiellement du temps requis pour exécuter ses différentes fonctions.

Figure 5 : Représentation schématique du déroulement cyclique

La programmation de l'approche cyclique est relativement simple. Les fonctions sont appelées l'une après l'autre dans la boucle principale de la fonction main(). Les événements (par exemple, l'actionnement d'un commutateur ou la réception de données via l'interface série) sont scrutés périodiquement dans cette boucle (anglais : polling). Néanmoins, les spécifications pour les applications embarquées exigent souvent que les événements soient traités dans un délai très court, ce qui n'est pas forcément possible avec l'approche cyclique décrite ici.

III-B - Déroulement quasi parallèle et événementiel

Les événements peuvent interrompre le processus cyclique en déclenchant une interruption. La routine de service d'interruption (abréviation en anglais : ISR) est ainsi appelée en temps réel pour traiter l'événement. Le déroulement du programme devient ainsi quasi parallèle et événementiel.

Figure 6 : Déroulement quasi parallèle avec des événements.

Cette approche est souvent utilisée dans les systèmes embarqués. En effet, les interruptions sont relativement faciles à programmer. Les critères temps réel du système sont ainsi bien remplis. Néanmoins, il subsiste le problème de la communication entre l'ISR et les fonctions de la boucle principale. En effet, cette communication nécessite le recours aux variables globales. Il est important d'exclure les accès mutuels à ce genre de variable de sorte qu'une partie du programme ne puisse y accéder à un instant donné. Cela signifie qu'une fonction de la boucle principale doit verrouiller l'interruption (dont la routine de service pourrait également accéder à cette variable) durant l'accès à cette variable. Pour des applications plus complexes, qui nécessitent plusieurs interruptions et variables globales, cela peut conduire à des interdépendances dissimulées et entraîner des crashs du système. Des exemples pour ce genre de problème, qui peuvent engendrer des conditions dites de course, sont fournis à la section IV.C.6.

III-C - Déroulement quasi parallèle avec RTOS

Les applications embarquées plus complexes sont souvent réalisées avec des systèmes d'exploitation temps réel (abréviation anglaise : RTOS). Bien que ces systèmes nécessitent plus de ressources que l'approche quasi parallèle et événementielle, ils offrent néanmoins des outils supplémentaires pour résoudre les problèmes décrits dans la section précédente. Les RTOS ne sont pas traités dans ce cours.

III-D - State-Event

III-D-1 - Introduction

Le comportement de la plupart des systèmes techniques peut être décrit avec des machines d'état (anglais : State Machine). Ce chapitre traite la conception d'une machine d'état à l'aide de diagramme d'état et sa programmation en C. Les exemples illustratifs se basent sur une connexion téléphonique simplifiée.

Les systèmes techniques possèdent un nombre fini d'états, dans lequel ils peuvent se trouver à un instant donné. Ils réagissent à des événements qui peuvent se produire durant leur fonctionnement. En fonction de l'état actuel du système, ces événements peuvent déclencher des activités et éventuellement générer un changement d'état de ce dernier.

Définitions :

- Un état (state) est une disposition, dans laquelle le système entre durant une période limitée. Différentes activités (activity) peuvent être exécutées au sein d'un état. Exemples d'état : « Sonnerie », « Conversation », etc.

- Un événement (event) est une influence extérieure sur le système ou un changement dans le système. Un événement est de courte durée (quantité de temps négligeable) et a toujours un impact sur le système. Exemples d'événements : « combiné est décroché », « numéro est sélectionné », etc.

- Une transition décrit le passage d'un état à un autre. Une transition est toujours déclenchée par un événement. Exemple de transition : Le décrochement du combiné (événement) change l'état du système de « Téléphone libre » à « Saisir le numéro ».

- Une action est exécutée au cours d'une transition. La transition « Téléphone libre » à « Saisir le numéro » augmente la tonalité.

III-D-2 - Conception

III-D-2-a - Tableau d'états

Les tableaux d'états constituent un moyen simple mais pas très compréhensifs de décrire les états d'un système. Tous les états, événements, actions et les transitions sont définis sous forme texte dans un tableau. Les tableaux d'états permettent de décrire entièrement le comportement d'un système.

Tableau 1 : Tableau d'états pour le déroulement d'une communication au téléphone.

État actuel

Événement

Action

Nouvel état

Téléphone libre

Combiné est décroché

Activer la tonalité bip

Saisir le numéro

Saisir le numéro

Combiné est raccroché

Pause

Téléphone libre

Chiffre est sélectionné

Transmettre le chiffre

Saisir le numéro

Numéro libre est sélectionné

Activer la tonalité d'appel

Sonnerie

Numéro occupé est sélectionné

Activer la tonalité occupée

Occupé

Sonnerie

Combiné est décroché

Pause

Téléphone libre

Correspondant s'annonce

-

Conversation

Occupé

Combiné est raccroché

Pause

Téléphone libre

Conversation

Combiné est raccroché

Pause

Téléphone libre

III-D-2-b - Diagramme d'état

Les diagrammes d'état permettent de représenter les différents états et transitions du système sous forme graphique. Par conséquent, ces derniers sont beaucoup plus compréhensifs que les tableaux d'états. Il existe différentes approches pour la représentation graphique. Ce cours se base sur le standard UML (V2.0), avec lequel deux états et une transition seront représentés de la manière suivante :

Figure 7 : Diagramme d'état simplifié selon le standard UML (V2.0).

Les états sont représentés par des rectangles arrondis qui contiennent leur nom. Les activités, qui sont exécutées dans un état donné, sont indiquées en dessous du nom.

Les transitions sont indiquées par des flèches entre les états. Une transition est exécutée lorsqu'un événement se produit et que la condition requise se réalise. Une action peut être également définie pour les transitions.

La définition des actions, des conditions et des activités est en option.

Notre exemple d'appel téléphonique peut être représenté de la manière suivante avec un diagramme d'état :

Figure 8 : Diagramme d'état pour le déroulement d'une communication au téléphone.

Les diagrammes d'état peuvent également être imbriqués l'un dans l'autre. Cela est particulièrement utile quand, comme ci-dessus, les mêmes événements (Combiné est raccroché) permettent d'accéder à partir de plusieurs états à un seul état (Téléphone libre). La représentation peut être ainsi simplifiée.

Figure 9 : Diagramme d'état emboîté pour le déroulement d'une communication au téléphone.

III-D-3 - Implémentation de diagrammes d'état

Il existe plusieurs approches pour implémenter les diagrammes d'état. Deux d'entre elles sont brièvement abordés dans ce chapitre. Si vous définissez un projet en C++, vous avez la possibilité d'utiliser les modèles de conception d'état (anglais : State Design Pattern). Ces derniers ne sont pas abordés dans ce cours.

III-D-3-a - Instructions switch emboîtées

Une bonne approche et couramment utilisée pour programmer les machines d'état est l'imbrication de deux structures switch. La structure externe permet de gérer les états possibles alors que la structure interne est responsable du traitement des différents événements. Le code C se présente ainsi de la manière suivante :

switch(state){
case STATE_1:
switch(event){
case EVENT_1:
action_1();
state = STATE_V;
break;
case EVENT_2:
action_2();
state = STATE_W;
break;
…
}
break;
case STATE_2:
switch(event){
case EVENT_3:
action_3();
state = STATE_X;
break;
case EVENT_4:
action_4();
state = STATE_Y;
break;
…
}
break;
…
}

III-D-3-b - Les tableaux

Une manière élégante consiste à stocker toutes les actions et les états suivants dans un tableau à deux dimensions. Le tableau a la structure suivante :

E1

E2

…

En

E1 … En : Événement

A1/Sv

…

…

…

S1

S1 … Sn : État

…

…

…

…

S2

A1 … An : Action

…

…

…

…

…

Sv : État suivant

…

…

…

…

Sn

Ce tableau contient pour chaque combinaison possible d'événements et d'états une structure. Cette structure quant à elle contient une information concernant l'action à exécuter et l'état suivant. Le code C se présente en général comme suit :

// typedef pour State et Event
typedef enum {S1, S2, S3} StateType;
typedef enum {E1, E2, E3, E4, E5, E6, E7, E8, E9} EventType;

/* Déclaration d'un élément du tableau d'états,
qui contient l'action à exécuter et l'état suivant */
typedef struct tab {
int(*action)(void*);
StateType nextState;
}TabElement;

// Déclaration de toutes les actions
int Action1(void* para);
int Action2(void* para);
int Action3(void* para);
…

// La définition du tableau d'états
TabElement stateTable[3][3] = {
// E1 E2 E3
{ {Action1,S2},{Action2,S2},{Action3,S3} }, // S1
{ {Action4,S1},{Action5,S3},{Action6,S3} }, // S2
{ {Action7,S1},{Action8,S2},{Action9,S3} }, // S3
};

// La définition des actions
int Action1(void* para){
...
return(0);
}

int Action2(void* para){
...
return(0);
}

int Action3(void* para){
...
return(0);
}

...

void main(void){
int para = 0;
// Événement et état de départ
EventType event = E1;
StateType state = S1;
while(1){
// Code à exécuter à chaque événement
(*(stateTable[state][event]).action)(&para); // Exécuter l'action
state = (stateTable[state][event]).nextState; /* Lire le prochain état */
}
}

IV - Sécurité des systèmes

IV-A - Introduction

Les exigences de qualité pour les systèmes embarqués sont très élevées. Ces exigences concernent le « cycle de vie » complet du produit : depuis la planification en passant par le développement jusqu'à la formation des clients. Voici quelques commentaires concernant la partie matérielle (hardware) et la partie logicielle (software).

IV-A-1 - Hardware

La qualité de la partie matérielle (hardware) peut être déterminée statistiquement. Les probabilités de défaillance (MTTF Mean Time To Fail, ou MTBF, Mean Time Between Failure) des composants sont en général connues. Cela permet de définir la probabilité de défaillance du système en entier ou le temps nécessaire pour que celui-ci atteigne un état jugé comme étant dangereux.

La qualité de la partie matérielle peut être augmentée grâce à la redondance. La notion de « fail-safe mechanism » est également utilisée dans ce genre de système. La redondance vise principalement à réaliser des systèmes (hardware et software) sûrs et fiables. Ces notions sont souvent utilisées mais elles n'ont pas les mêmes significations. Les systèmes sûrs sont destinés à protéger les Hommes. Quant aux systèmes fiables, ils doivent uniquement réduire leur probabilité de défaillance afin de réduire les coûts. Ce qui ne signifie pas nécessairement plus de sécurité. Les options suivantes peuvent être envisagées en fonction des champs d'application :

- surveillance de la partie logicielle à l'aide d'un Watchdog (voir chapitre IV.E) ;

- surveillance de la partie matérielle (horloge et alimentation) par des composants supplémentaires ;

- surveillance de la partie matérielle par la partie logicielle (CRC, Test de la RAM, etc.) ;

- partie matérielle redondante (dédoublement d'entrée/sortie, deux processeurs en parallèle ou trois avec une stratégie de décision majoritaire) ;

- partie logicielle redondante (des programmes différents sont exécutés sur des processeurs différents).

IV-A-2 - Software

Le développement du logiciel peut être amélioré qualitativement par des mesures appropriées. Contrairement aux logiciels bureautiques, où les utilisateurs se sont résignés aux problèmes de logiciels (le nom du système d'exploitation a été délibérément omis ici), la fiabilité des logiciels embarqués doit être très élevée. Ces exigences sont encore plus importantes pour les applications liées à la sécurité. Le développement de logiciels de bonne qualité n'est possible qu'avec plusieurs mesures :

- des ingénieurs bien formés et qualifiés qui ont une bonne connaissance des pièges courants ;

- révision des spécifications, de la conception et du code ;

- une conception du logiciel de haute qualité ;

- code de haute qualité grâce à l'utilisation des normes et des directives SW ;

- mécanismes de détection d'erreur dans le logiciel ;

- utilisation des outils d'analyse de code statique ;

- des tests approfondis durant la phase de développement ;

- tests du système complet avant et pendant la mise en service ;

- une documentation complète, compréhensible et actualisée.

IV-B - Le langage de programmation C

Un critère (mais pas le seul) pour la qualité des logiciels est le langage de programmation. Le langage C, qui est le plus couramment utilisé dans les systèmes embarqués, n'est malheureusement pas sûr au niveau des types et présente également de nombreux autres pièges. Par exemple, les pointeurs en C ont déjà inquiété de nombreux experts en sécurité (et également les programmeurs). Toutefois, tout n'est pas perdu, si en tant que programmeur, vous êtes conscient du problème. En effet, il faut prendre les précautions nécessaires durant la programmation. Par exemple, il est fortement recommandé de ne jamais utiliser qu'un sous-ensemble du langage de programmation C. Cette recommandation est valable, non seulement pour des systèmes de sécurité, mais pour tous les programmes d'utilité générale. Une liste exhaustive de telles recommandations est fournie par l'association MISRA-C [MISRA-C]. Certaines d'entre elles sont décrites dans la section suivante.

La question qui pourrait être posée ici est la suivante : pourquoi faut-il utiliser le langage C pour programmer les systèmes embarqués, alors que ce dernier est lacunaire. Pour cela, il y a deux raisons importantes :

- Premièrement, dans domaine de la programmation des microcontrôleurs, il n'existe (presque) pas d'alternative. Il faut donc composer avec cela.

- Deuxièmement, il est plus opportun d'utiliser un langage de programmation que l'on connaît (en particulier ses faiblesses), pour lequel il existe de bons compilateurs, qu'un langage qui n'est pas répandu et pour lequel il n'existe que de mauvais outils de développement.

IV-B-1 - MISRA-C

L'association MISRA-C (Motor Industry Software Reliability Association) définit les directives pour l'utilisation du langage de programmation C dans l'industrie automobile, qui prévalent également dans d'autres secteurs de l'industrie. Le titre officiel est « Guidelines for the use of the C langage in critical systems ». www.misra.org.uk

MISRA-C 122 définit les règles « nécessaires » et 20 règles « consultatives ». La figure ci-dessous illustre deux de ces règles. Toutes les règles doivent être respectées pour pouvoir être conformes aux directives MISRA.

Figure 10 : Exemples de règle MISRA-C.

IV-B-2 - Analyse de code statique

Les compilateurs C sont en général très tolérants (ou conviviaux) durant la compilation. Ils « ferment plusieurs fois les yeux » pour que le code puisse être compilé et ainsi s'exécuter plus rapidement. Mais cela peut s'avérer fatal durant le fonctionnement ! Ce qui est bien entendu beaucoup moins convivial et très fâcheux en termes de qualité. Il existe différents outils (vérificateurs de syntaxe) sur le marché, qui permettent d'effectuer des analyses statiques du code afin de fournir des messages d'erreurs, des avertissements ou des informations générales sur le code.

Une bonne approche consiste à faire contrôler votre code à l'aide d'un vérificateur de syntaxe. Les nombreuses réclamations fournies par ce dernier vont peut-être vous étonner. Ne soyez toutefois pas frustré mais montrez-vous au contraire reconnaissant : en effet la phase de test sera d'autant plus facile. Les analyses du code à l'aide d'un vérificateur de syntaxe devrait commencer au début - et non à la fin - de la phase de programmation afin d'éviter un flot trop important d'avertissements.

Il existe plusieurs produits sur le marché comme « PC-Lint » de Gimpel. De tels outils permettent également d'examiner si les règles de MISRA-C sont respectées (pas forcément toutes les règles, mais la plupart qui sont spécifiées par le fabricant).

IV-C - Murphy et le langage de programmation C

Ce chapitre répertorie quelques problèmes typiques, qui peuvent survenir avec le langage de programmation C.

Ces indications ne sont pas exhaustives. Elles illustrent cependant quelques-uns des principaux problèmes de ce langage.

IV-C-1 - Dépassement de capacité dans les tableaux

En C, vous pouvez accéder à un tableau en dehors de ses limites, sans que vous puissiez vous en rendre compte (ou juste avant un crash du système). Considérez par exemple le code suivant qui est syntaxiquement correct (c'est-à-dire que le compilateur va probablement le compiler sans avertissement) :

char array[10];
char counter = 0;
…
for (counter = 0; counter <= 10; counter++){
array[counter] = 0;
}

Dans le code ci-dessus, vous dépassez les limites du tableau «array» lorsque « counter = 10 ». Quelle est la conséquence de cela ? Dans le pire de cas, le relieur (anglais : linker) réserve de l'espace mémoire pour la variable counter directement à la suite du tableau array. Dans ce cas l'instruction « array[counter] = 0» efface systématiquement le contenu de la variable counter. La boucle for devient ainsi une boucle infinie. À cet égard, il se peut que vous ayez de la chance et que vous trouviez cette erreur durant les premières phases de test. Dans d'autres cas, l'identification du problème peut s'avérer beaucoup plus chère.

Il existe deux approches pour résoudre ce problème :

- Vous portez une attention particulière aux limites du tableau pendant le codage. Vous pouvez également contrôler l'index du tableau avant d'accéder à ce dernier. Vous pouvez également démontrer que vous n'avez pas dépassé les limites du tableau à l'aide de tests approfondis. Malheureusement, une certaine incertitude demeure toujours.

- Ajoutez un élément supplémentaire au tableau, que vous initialiserez avec une valeur spécifique (par ex. 0x55). Testez à présent le système et contrôlez ensuite si la valeur de ce dernier élément n'a pas été modifiée.

IV-C-2 - Dépassement de capacité dans la pile (anglais : stack)

Ce problème est identique à celui lié aux dépassements de capacité dans les tableaux. Lorsque vous dépassez les limites de votre pile (trop d'appels de fonctions imbriquées ou trop de variables locales), vous risquez d'accéder aux segments de la RAM qui contiennent des variables. Les choses peuvent encore se gâter lorsque le programme est exécuté à partir d'une RAM. En effet, dans ce cas, vous risquez d'écrire par-dessus le code.

Les erreurs qui résultent de ce genre de dépassement vont se manifester à un instant donné (en fonction des objets réécrits et de l'utilisation de ces derniers). Ce qui rend la recherche de telles erreurs encore plus difficile.

Comment un débordement de la pile peut-il être déterminé ?

Initialisez votre pile au démarrage du système avec des valeurs spécifiques, par exemple 0x55. Après un test exhaustif du système, vous pouvez alors déterminer la part de la pile qui a été utilisée par votre programme (la limite se situe à l'endroit où les valeurs spécifiques d'initialisation 0x55 ont été réécrites).

Quelles sont les solutions pour résoudre ces problèmes ?

- Agrandissement de la taille de la pile. De nombreux environnements de développement permettent de définir la taille de la pile à l'aide d'options pour le relieur (anglais : linker).

- Réduction de la taille des variables locales dans vos fonctions.

- Évitez les appels de fonction récursive !

IV-C-3 - Pointeur NULL

Un pointeur qui n'est pas initialisé adresse la case mémoire NULL (où même quelque part ailleurs). Une allocation de mémoire pour construire une liste dynamique, qui échoue par manque de mémoire disponible, retourne également l'adresse NULL. Le premier cas est une erreur de programmation classique. Le deuxième cas n'est pas une erreur, mais il peut se produire à n'importe quel instant.

Quelles sont les solutions pour résoudre ces problèmes ?

Il faut comparer la valeur de chaque pointeur avec l'adresse NULL avant de l'utiliser ! Cette vérification peut se réaliser soit avec : if(pointer != NULL), ou avec : assert(pointer != NULL).

IV-C-4 - Les interruptions

Un microcontrôleur possède de nombreux vecteurs d'interruption, qui sont rassemblés dans son tableau des vecteurs d'interruption. Normalement, seule une partie de ces vecteurs est utilisée par votre programme (Timer, interface série et peut-être encore deux ou trois ports IRQ) alors que la grande partie restante ne l'est pas. Que se passe-t-il alors quand apparaît une interruption qui n'a pas été prévue et n'est par conséquent pas traitée (par exemple la division par zéro) ? Dans ce cas, le contrôleur va accéder au tableau des vecteurs d'interruptions afin de charger l'instruction correspondant à cette interruption. Ce vecteur peut contenir avec de la chance la valeur zéro. Le programme fait ainsi un saut à l'adresse NULL, ce qui correspond en général à une réinitialisation du système (anglais : reset). Toutefois ce vecteur peut également contenir une valeur différente de zéro. Le programme fait dans ce second cas un saut à une adresse inconnue, qui engendrera probablement un crash du système.

Quelles sont les solutions pour résoudre ce problème ?

Il faut toujours initialiser tous les vecteurs d'interruption. Si vous n'utilisez pas une interruption donnée, implémentez pour cette dernière une routine de traitement d'interruption (anglais : ISR) par défaut. Dans cette routine de service, vous pouvez afficher un message d'erreur ou implémenter une boucle infinie pour le débogage.

IV-C-5 - Allocation de mémoire dynamique

La mémoire est allouée dynamiquement dans les programmes afin de générer des listes. Cela marche bien tant que la mémoire disponible est suffisante. Des problèmes apparaissent inévitablement lorsque la mémoire requise n'est plus disponible. En d'autres termes, le programme n'a plus de ressource en mémoire. Un autre problème est la fragmentation de la mémoire qui peut survenir avec l'allocation de la mémoire dynamique.

Quelles sont les solutions pour résoudre ce problème ?

- Essayez de définir les structures de données autant que possible de façon statique.

- Assurez-vous que la mémoire, qui a été allouée dynamiquement, soit également de nouveau libérée.

Vous devez vous en occuper vous-même en C et en C++.

- Testez le système également en termes de ressources en mémoire : exécutez votre programme durant des heures ou des jours et regardez ensuite si l'espace mémoire disponible reste constant.

IV-C-6 - Les conditions de course

Les conditions de course (anglais : race condition) signifient les situations de course. Lorsque les conditions de course sont remplies, les résultats du programme dépendent du comportement temporel de ce dernier. Les situations de course surviennent lorsqu'il y a une interaction asynchrone entre la partie matérielle (hardware) et la partie logicielle (software). C'est le cas par exemple avec les Timers ou les convertisseurs A/D. Dans ces cas, les conditions de course se produisent lorsque les données sont lues systématiquement par le logiciel, alors qu'elles ont été changées de manière asynchrone par le hardware.

Exemple Timer : Le code suivant génère des conditions de course (référence : [Firmware_Handbook]) :

unsigned int timer_hi;
interrupt timer(){
++timer_hi
}

unsigned long read_timer(void){
unsigned int low, high;
low = read_word(timer_register);
high = timer_hi;
return (((unsigned long)high)<<16 + (unsigned long)low);
}

Le code ci-dessus est erroné. En effet, admettons que l'on commence par lire la valeur du registre « timer_register ». Ce registre contient une information sur la partie fractionnaire du temps qui s'écoule.

Supposons qu'ensuite se produit un dépassement de capacité engendrant l'incrémentation de la variable « timer_hi ». Cette variable contient une information sur la partie entière du temps. La combinaison de ces deux variables conduit ainsi à une information erronée sur le temps.

Le problème peut être résolu à l'aide d'approches différentes :

- considération du problème dans la phase de conception (anglais : design) ;

- désactiver systématiquement les interruptions durant les accès au matériel (anglais : hardware) ;

- utilisation des « registres de capture » (anglais : Capture-Register).

Les conditions de course sont souvent à l'origine d'erreurs logicielles, qui sont très difficiles à localiser (exemple du Timer ci-dessus) !

IV-C-7 - Code réentrant

Les applications embarquées sont toujours programmées à l'aide d'interruptions ou parfois également avec des systèmes d'exploitation temps réel. Cela signifie qu'une fonction peut être interrompue à tout instant par une interruption ou par une autre tâche (anglais : task). Le chaos est assuré à partir du moment où les différentes parties du programme accèdent sans précautions spécifiques aux mêmes ressources (interfaces matérielles, variables globales). Par conséquent, les fonctions doivent être programmées afin qu'elles soient réentrantes. C'est-à-dire que :

- les variables globales ne doivent être accessibles que de façon exclusive (« atomique ») ;

- il ne faut pas appeler les fonctions non réentrantes (attention avec les fonctions des librairies standard) ;

- les interfaces matérielles (anglais : hardware interfaces) ne doivent être accessibles que de façon exclusive.

La meilleure façon d'obtenir l'accès exclusif, c'est de désactiver les interruptions durant l'accès. Toutefois, cela implique une augmentation des temps de latence pour le traitement des interruptions. Par conséquent, les accès doivent être aussi courts que possibles.

Dans ce contexte la règle importante à respecter est la suivante :

« Share the code, not the data » !

IV-C-8 - Programmation défensive

La programmation défensive signifie que l'on considère toutes les éventualités : arguments transmis aux fonctions non plausibles, transmission de données erronées, séquences temporelles non prévues, etc.). Lorsque vous programmez de façon défensive, ce qui est particulièrement recommandé pour des systèmes où la sécurité est essentielle, vérifiez tout auparavant. Cela signifie en particulier que :

- la gamme de valeur des arguments transmis aux fonctions doit être contrôlée au début de ces dernières ;

- les transmissions de données doivent être vérifiées systématiquement (Check sum) ;

- la plausibilité des résultats doit être vérifiée.

La programmation défensive permet d'augmenter la sécurité du système. Cela n'est cependant pas gratuit. En effet cette approche nécessite plus de code et, par conséquent, plus de puissance de calcul, plus de capacité de stockage et plus de développement.

IV-C-9 - assert()

assert() est une macro de la bibliothèque standard. assert() permet d'évaluer de façon très simple et efficace diverses conditions durant l'exécution du programme :

assert(expression);

Lorsque la condition évaluée se révèle être fausse (valeur nulle), la macro assert() livre un message d'erreur à travers le canal stderr. Ce message a typiquement la forme suivante :

Assertion failed: expression, file filename, line nnn

La macro assert() sera ignorée lorsque la macro NDEBUG est définie avant l'inclusion de la bibliothèque standard <assert.h>.

Les exemples d'application de la macro assert() sont les tests de pointeur (pointeur non nul) ou de gamme de valeurs pour les paramètres, etc.

IV-C-10 - Optimisation du code C

Les compilateurs C essayent de générer un code objet le plus optimisé possible. Cela signifie que ce dernier doit être à la fois aussi rapide et compact que possible. Le résultat du compilateur peut également être influencé de façon significative par la manière dont le code C est défini :

1. Essayez d'éviter autant que possible les variables globales. Définissez plutôt les variables locales à l'intérieur des fonctions. Le compilateur peut ainsi stocker ces dernières dans des registres locaux - plutôt que de les placer dans la mémoire externe (RAM), ce qui diminue le temps d'accès aux variables. Essayez également de réduire autant que possible le nombre de variables locales. En effet, le nombre de registres est limité et les variables, qui ne peuvent pas être stockées dans des registres, sont placées sur les piles. Cependant, si vous avez toujours besoin de nombreuses variables locales, essayez d'utiliser ces dernières de façon limitée à l'intérieur de la fonction. Ainsi, le compilateur peut attribuer un seul registre aux diverses variables durant l'exécution de la fonction.

2. Évitez les opérations de prises d'adresse avec les variables locales. En effet, cela empêche le compilateur de les stocker dans des registres.

3. L'assembleur en ligne ne devrait être utilisé qu'en dernier recours. En effet, le compilateur n'a pas le droit d'optimiser le code C autour de ces derniers. Utilisez plutôt les sous-routines en assembleur.

4. Évitez de définir des fonctions avec des listes de paramètres variables (comme printf). En effet, l'appel de ces fonctions nécessite en général plus de ressources systèmes.

5. Remplacez les fonctions très courtes (1 à 3 lignes de code) par des macros ou des fonctions en ligne. Les ressources systèmes nécessaires à l'appel des fonctions peuvent ainsi être réduites au maximum. En effet, le compilateur copie littéralement les lignes de code des fonctions en ligne aux endroits appropriés dans le programme. Ce qui d'une part augmente considérablement la vitesse d'exécution du programme et d'autre part - surtout avec les fonctions très courtes - réduit le nombre d'instructions assembleur.

6. La transmission des arguments aux fonctions devrait être réalisée à l'aide de paramètres au lieu de variables globales. En effet, dans le premier cas, le compilateur utilise les registres pour transmettre les arguments. Cette procédure est beaucoup plus rapide que l'accès systématique aux variables globales, qui sont stockées dans la mémoire externe (RAM). Les premiers arguments sont en général transmis avec des registres et les suivants avec la pile (anglais : stack). Par conséquent, il faut faire également attention au nombre d'arguments qui vont être transmis à la fonction.

7. Si vous souhaitez transmettre des structures de données comme paramètre à des fonctions, transmettez ces dernières avec des pointeurs constants (avec le mot clé «const»). Le compilateur déposerait autrement la structure entièrement sur la pile.

8. Utilisez le mot-clé «volatile» pour indiquer au compilateur que la variable ne peut pas être stockée dans un registre. Cette dernière sera ainsi toujours déposée dans la mémoire externe (RAM). Cette définition est même indispensable pour les variables qui permettent d'accéder à la partie matérielle (anglais : hardware) comme le « Timer ».

9. Essayez les différents niveaux d'optimisation du compilateur. Le niveau le plus haut ne génère ici pas forcément le code le plus rapide ou le plus compact.

10. Utilisez toujours, pour la définition des variables, le type de données le plus adéquat en fonction de la taille de la CPU (8, 16 ou 32 bits). Autrement, le compilateur doit effectuer une, voire plusieurs opérations cast. Les opérations arithmétiques avec le type char sont en principe très efficaces sur les microcontrôleurs 8 bits. Avec ces derniers, les opérations 16 ou 32 bits nécessitent des fonctions de la bibliothèque standard, ce qui n'est pas efficace. Un microcontrôleur 8 bits est en général dépassé avec les calculs à virgule flottante. Les fonctions de la bibliothèque standard sont alors très complexes et leur temps d'exécution très long.

11. Les microcontrôleurs 32 bits possèdent souvent des contraintes d'alignement pour les adresses des variables. Essayez de déclarer systématiquement les membres 32 bits en premier, les membres 16 bits en second et les membres 8 bits à la fin dans les structures de données. Cela empêche le compilateur de devoir insérer des espaces libres (« padding ») entre les différents types de membres, ce qui a tendance à augmenter la taille de la structure de données.

12. Ne définissez pas de code « intelligent », qui n'est pas compréhensible pour votre collègue (et pour vous après quelques jours). Le compilateur ne comprend pas ce code et ne peut donc l'optimiser. Définissez votre code plutôt de façon « simple et continue ». « Write simple and understandable code » !

13. Définissez une structure « switch » au lieu d'un tableau de saut (anglais : Jump-Table). En effet, le compilateur va générer un tableau de saut optimal pour le microcontrôleur sélectionné pour cette instruction. Le code reste ainsi parfaitement portable.

IV-D - Sécurité au niveau du système

Les parties matérielles et logicielles ne sont pas uniquement critiques pour la sécurité du système. En effet, cette dernière dépend du cycle de vie complet du produit, depuis sa spécification jusqu'à la formation des clients.

Les paragraphes suivants traitent quelques points qui concernent surtout la phase de développement.

IV-D-1 - La conception

Comment les améliorations peuvent-elles être intégrées sans problèmes ? La structure finale d'un programme dépend de plusieurs critères. Le critère le plus important est celui de la mise en œuvre des spécifications. Toutefois, la conception peut également être influencée par le programmeur lui-même. Les expériences et les compétences de ce dernier jouent ici un rôle majeur. C'est un peu la discipline reine de l'ingénieur logiciel, la suite n'est plus que du codage et des tests. Les erreurs de conception sont en général très problématiques. La correction de ces dernières nécessite souvent beaucoup d'efforts. En effet, une grande partie du code doit être modifiée et les tests doivent être effectués à nouveau.

La conception a un impact majeur sur la qualité du code. Cette phase doit inclure les éléments suivants :

- la modularisation de votre système en fichiers / classes / tâches ;

- la description des interfaces de ces modules ;

- la communication entre ces modules ;

- le comportement temporel des différents modules et de l'ensemble du système ;

- les interfaces au processus technique et à l'utilisateur.

IV-D-2 - Spécification et exécution des tests

Les tests ne devraient pas être simplement réalisés à la fin du projet avec la devise : démarrer le système - ce dernier fonctionne - donc tout est bon. Les tests doivent plutôt être spécifiés durant la conception du système.

Cette spécification doit contenir les types, les procédures et les paramètres des tests. Elle peut être obtenue à partir des spécifications du système. La rédaction des spécifications de test devrait être réalisée de préférence durant la phase de conception du système ou, dans tous les cas, avant la phase de codage.

Des tests sont particulièrement importants pour les systèmes embarqués. Les erreurs, qui ne sont pas trouvées durant la phase des tests, sont très coûteuses, voire dangereuses. Dans ce cas, vous devez rappeler les systèmes vendus (en effet, vous ne pouvez pas envoyer simplement une mise à jour du logiciel) ou installer une nouvelle version du logiciel chez le client. La règle d'or stipule que si vous avez programmé durant un mois, vous devriez également tester durant un mois !

Les tests sont toujours réalisés sur plusieurs niveaux : ils sont effectués individuellement sur les fonctions au niveau des modules ; puis sur les différentes parties du système et finalement sur le système dans son ensemble dans un environnement réel. Les ingénieurs logiciels sont en général beaucoup trop indulgents face à leur propre code. Mais ces derniers souhaitent-ils vraiment que quelqu'un d'autre trouve les erreurs à leur place ? Les grandes entreprises emploient des ingénieurs de test : ces derniers se réjouissent toujours lorsqu'ils trouvent de nouvelles erreurs. Mais, il est en général beaucoup plus agréable de trouver ses erreurs soi-même.

Par conséquent : essayez de vraiment torturer votre logiciel et d'aller à ses limites. Ici vous serez certainement beaucoup plus créatif ou plutôt plus destructif qu'une personne tierce.

Des nombreux ouvrages ont été publiés sur les modèles et les procédures de tests. Voici quelques points clés en bref :

- Vérifier en particulier les interfaces pour les utilisateurs et pour le processus technique (les capteurs, les convertisseurs, les interfaces utilisateur d'entrée et de sortie, etc.). Vous ne pouvez sûrement pas tester toutes les combinaisons possibles d'interfaces utilisateur. Mais quelques types de clavier devraient par exemple être possibles.

- Vérifier en profondeur les interfaces de communication. Si vous avez par exemple une interface sérielle, alors bombardez votre système avec des demandes, envoyez-lui également des protocoles non définis, etc.

- Vérifier le comportement de votre système même en cas de surcharge.

Une bonne approche consiste à travailler avec deux versions du code : une version pour un fonctionnement standard et une version pour le débogage. Cette dernière contient du code supplémentaire pour exécuter les tests. Cela peut être défini en C avec une macro de la manière suivante :

#define _DEBUG
...
statement;
...
#ifdef _DEBUG
// additional testing and error messages output
#endif

Vous pouvez commuter entre les versions en commentant l'instruction « #define_DEBUG ». Le code pour les deux versions est ainsi défini dans les mêmes fichiers.

Il existe de nombreuses normes et directives pour effectuer des tests logiciels, dont les plus recommandées sont les suivantes :

- IEEE 829, Standard for Software Test Documentation ;

- IEEE 1008, Standard for Software Unit Testing ;

- IEEE 1012, Standard for Software Verification and Validation.

IV-D-3 - La documentation

Une bonne documentation permet de s'assurer que :

- les changements ou les améliorations peuvent être effectués sans erreurs ;

- toutes les erreurs peuvent être localisées plus facilement ;

- vos collègues peuvent être intégrés dans le développement logiciel, sans que ces derniers ne vous posent continuellement des questions (et sans que ces derniers ne vous dérangent) ;

- les tests peuvent être répétés ou complétés.

La documentation est votre carte de visite. Si, par manque de temps ou de ressources, vous renoncez à définir une documentation, vous le regretterez plus tard (par exemple, lorsque vous devez remettre à jour votre code).

Si, par manque de temps ou de ressources, vos supérieurs exigent une documentation réduite, alors défendez-vous dans votre propre intérêt.

Une bonne documentation constitue un autre avantage à ne pas sous-estimer. Si une erreur de votre logiciel a causé des dommages à des biens ou des personnes, vous vous trouverez en tant que développeur sans bonne documentation en manque de preuve. Aujourd'hui, les erreurs logicielles ne peuvent pas être exclues. Mais une bonne conception, un bon rapport et des tests bien documentés peuvent prouver que vous avez fait tout votre possible pour éviter les erreurs.

La forme de la documentation est secondaire. L'important est que tout y est décrit. Cela commence par la conception, en incluant les différentes variantes et leur évaluation. Cette dernière est suivie par la description du code sans oublier la description compréhensible des tests.

IV-D-4 - Reviews

Effectuez régulièrement des réexamens (reviews) durant le développement du projet. Cela s'applique aux spécifications (en incluant également les spécifications de test), la conception et le code.

Les réexamens du code ne sont pas effectués dans de nombreuses entreprises « par manque de temps ». C'est très dommage. En effet, le temps investi dans les réexamens est gagné par la suite durant le débogage. Certaines erreurs typiques, telles que les conditions de course, ne peuvent être isolées par les développeurs expérimentés que durant les phases de réexamens. Un autre avantage des réexamens est l'échange de connaissances entre les développeurs.

IV-E - Watchdog

IV-E-1 - Introduction

La tâche du Watchdog (français : horloge de surveillance) est de réinitialiser un microcontrôleur (c'est-à-dire de ramener le système à un état défini) à partir d'un état indéfini. Cet état est souvent le résultat d'un comportement erroné du programme, par exemple une boucle infinie. Dans le langage courant, on appelle cela un plantage informatique (anglais : system crash).

L'utilisation d'un watchdog est fortement recommandée pour tous les systèmes (même avec des applications très simples). Par conséquent, de nombreux microcontrôleurs contiennent un watchdog interne.

IV-E-2 - Le watchdog externe

Les watchdogs externes peuvent exécuter - en complément des applications de surveillance du processeur - les tâches suivantes :

1. Surveillance de l'alimentation du système.

2. Surveillance de l'horloge du système.

Les courtes interruptions d'alimentation sont en général très dangereuses pour les systèmes à microprocesseur.

En effet, ces dernières peuvent faire entrer le processeur dans des états non définis. Les watchdogs externes surveillent également l'alimentation et génèrent un reset en cas de problème. Cette fonctionnalité peut également être reprise par le module reset.

Les watchdogs externes possèdent en général leur propre base de temps. Ce qui leur permet de surveiller également l'horloge du système.

Le circuit de watchdog externe est réalisé typiquement de la manière suivante :

Figure 11 : Le watchdog externe

IV-E-3 - Le watchdog interne

Un watchdog interne (c'est-à-dire intégré dans un microcontrôleur) génère également un reset en cas d'erreur. Un tel reset ne se distingue en général pas de celui qui est généré de façon externe.

Selon la famille des microcontrôleurs, il existe des watchdogs internes qui possèdent leur propre base de temps.

Cette dernière est en général produite à l'aide d'un circuit RC.

IV-E-4 - Fonctionnement du watchdog

Le cœur du watchdog est un registre compteur, qui est remis à zéro périodiquement par le logiciel. Un dysfonctionnement dans cette procédure de rafraîchissement engendre un dépassement de capacité du compteur et déclenche une réinitialisation du système.

La figure suivante illustre la structure de base d'un watchdog :

Figure 12 : Fonctionnement du watchdog

IV-E-5 - Démarrage du watchdog

Le démarrage du watchdog s'effectue de deux manières :

- De façon hardware après le reset. Cela présente l'avantage que le processus de contrôle du microcontrôleur débute à son démarrage.

- De façon software en fixant la valeur des bits de contrôle appropriés. Cela devrait être réalisé le plus tôt possible dans le code (c'est-à-dire avant l'initialisation du système).

IV-E-6 - Rafraîchissement du watchdog

Le rafraîchissement du watchdog s'effectue également de façon logicielle en fixant la valeur des indicateurs (anglais : flag) appropriés. Dans ce contexte, il est très important que les opérations de rafraîchissement ne soient pas éparpillées au hasard dans le code. En effet, il est plutôt recommandé d'effectuer ces opérations une seule fois par cycle d'exécution du programme, par exemple à la fin de la boucle principale main().

La procédure de rafraîchissement peut être combinée avec des clés afin d'augmenter encore plus la sécurité du système. La combinaison des clés avec un watchdog permet de réaliser les fonctionnalités suivantes :

- contrôler le comportement temporel d'un programme ;

- vérifier si les fonctions les plus importantes ont effectivement été exécutées.

La procédure est la suivante : une ou plusieurs clés sont nécessaires. Les clés sont des variables communes, qui permettent de contrôler le déroulement du programme. Une valeur spécifique est attribuée à une clé dans toutes les fonctions importantes du système. Si votre programme comporte de nombreux points à contrôler, vous pouvez également utiliser une seule variable clé. Dans ce cas, vous pouvez systématiquement changer la valeur de votre clé à l'aide d'un polynôme de CRC. À la fin d'un cycle d'exécution du programme, la valeur de la clé vous assurera systématiquement que toutes les fonctions importantes ont été appelées dans un ordre adéquat. En effet, si cette dernière est correcte, vous pouvez effacer son contenu et rafraîchir le watchdog. Toutefois, si la clé est erronée, cela signifie que le programme s'est comporté de façon erronée. Dans ce cas, le watchdog ne doit pas être rafraîchi afin d'engendrer le redémarrage du système avec un reset.

V - Connexion de la périphérie

Ce chapitre traite la connexion du microcontrôleur aux composants périphériques, tels que les capteurs, actionneurs et éléments de commande.

V-A - Bus d'adresse / de données

C'est la manière la plus simple d'ajouter des composants périphériques qui ne sont pas disponibles dans le microcontrôleur. Des fonctionnalités supplémentaires peuvent ainsi être ajoutées aux systèmes embarqués comme : les convertisseurs A/D, les contrôleurs USB ou CAN. La communication entre le microcontrôleur et les composants périphériques s'effectue alors avec les bus d'adresse et de données. Par conséquent, il leur faut également attribuer des plages d'adresse dans le plan de mémoire.

Figure 13 : Connexion des composants périphériques avec les bus d'adresse et de données

V-B - Port d'entrée et de sortie numérique

Beaucoup de microcontrôleurs proposent des ports d'entrée et de sortie numériques (GPIO : General Purpose Input/Output). Ces derniers sont en général des entrées ou des sorties multifonctionnelles et sont parfois regroupés par groupe de huit ports (1 octet).

Ces ports permettent de lire directement les signaux numériques à leur entrée ou de piloter des étages d'amplification de sortie.

Lorsque des composants externes doivent être pilotés à l'aide des ports GPIO, il faut vérifier dans la fiche technique du microcontrôleur que ces derniers sont capables de fournir le courant nécessaire. En effet, le courant fourni par ces ports ne se limite en général qu'à quelques mA. Par conséquent, dans certains cas il faut ajouter un étage d'amplification externe.

Les ports doivent toujours être complétés de circuits de protection d'entrée et de sortie afin d'optimiser au mieux leur compatibilité électromagnétique (CEM).

Figure 14 : Utilisation de ports d'entrée et de sortie numériques

L'accès aux ports est relativement simple en C :

- Les différents pins d'un port peuvent être mis à 1 ou à 0 de façon individuelle avec des opérations logiques binaires ET ou OU. Par exemple : P1 |= 0x01; met le pin P1.0 à 1. Remarque : cette opération lit d'abord l'état actuel du port P1 avant de mettre le pin P1.0 à 1.

- Certains microcontrôleurs permettent de programmer les pins des ports bit par bit. Ce genre d'opérations peut être réalisé soit à l'aide d'instructions en Assembleur (ces instructions sont alors également soutenues par les compilateurs C : P1_0 = 1; met par exemple le pin P1.0 à 1) ou elles sont soutenues directement par le hardware (ex. bit-banding avec ARM).

V-C - RS232

L'interface RS232 est très répandue dans le domaine embarqué. Cette interface permet par exemple de gérer des affichages LCD ou des lignes de commande (Command Line Interface). La plupart des microcontrôleurs possèdent déjà une interface RS232. Toutefois, les tensions de sortie de ces dernières correspondent au niveau TTL. Par conséquent, un convertisseur de tension externe doit être ajouté au système.

Les interfaces RS232 sont accessibles à l'aide de registres.

- Les tampons de transmission et de réception contiennent systématiquement 1 octet. Les deux tampons possèdent souvent la même adresse, car il n'est possible d'écrire que dans le tampon de transmission et de lire qu'à partir du tampon de réception.

- Les registres de contrôle, qui permettent de définir les modes de fonctionnement (la fréquence de transmission (baud rate), le format des données, le bit de parité, etc.) et les interruptions.

Les interfaces RS232 soutiennent différents modes de fonctionnement. Par exemple, ils peuvent fonctionner soit en mode synchrone ou en mode asynchrone. La fréquence de transmission est souvent définie avec un Timer standard. Certaines interfaces RS232 possèdent toutefois leur propre générateur de fréquence. Le Timer peut ainsi être utilisé pour d'autres applications.

Le schéma bloc de l'interface RS232 est typiquement le suivant :

Figure 15 : Schéma bloc d'un module RS232

V-D - SPI

L'interface SPI (Serial Peripheral Interface) est un bus sériel à haut débit, destiné à la communication entre le microcontrôleur et la périphérie. Ce dernier est souvent utilisé pour la communication avec des extensions d'entrée et de sortie, des affichages LCD ainsi que des convertisseurs A/D et D/A. Il peut également être utilisé pour la communication entre microcontrôleurs.

La figure suivante illustre le principe de fonctionnement du SPI :

Figure 16 : Principe d'une connexion SPI

L'interface SPI est toujours utilisée en mode maître-esclave. Le maître est alors responsable de la génération de la fréquence d'horloge. Le SPI peut travailler de façon duplex à l'aide de deux lignes de transmission : MOSI (Master Out Slave In) et MISO (Master In Slave Out). Les esclaves peuvent être connectés soit de façon parallèle (c'est-à-dire que toutes les sorties des esclaves sont rassemblées et connectées à l'entré MISO du maître) ou de façon sérielle (la sortie d'un esclave est connectée à l'entrée du prochain esclave et la sortie du dernier esclave est connectée à l'entrée MISO du maître).

Le microcontrôleur écrit les données à transmettre dans un tampon de transmission. Ces dernières sont sérialisées à l'aide d'un registre à décalage (comme une transmission RS232). Les données reçues sont également converties à l'aide d'un registre à décalage. Le microcontrôleur peut alors lire ces données de façon parallèle dans le tampon de réception. Du fait que les interfaces SPI ont une bande passante relativement élevée, les tampons de transmission et de réception contiennent souvent plusieurs octets.

Figure 17 : SPI avec maître et 2 esclaves

V-E - I2C

V-E-1 - Aperçu

Le bus I2C (Inter Integrated Circuit) a été développé par l'entreprise Philips. Ce bus est utilisé pour connecter les composants périphériques comme l'EEPROM, les affichages LCD ou RTC (Real Time Clock) au microcontrôleur.

Le bus I2C est composé de deux fils, ce qui réduit la partie hardware de façon drastique. Ce bus comprend une ligne d'horloge SCL (Serial Clock) et une ligne de données SDA (Serial Data). La communication est par conséquent synchrone. Son mode d'utilisation est souvent du type maître et esclave. Cependant, il soutient également le mode multimaître.

La figure suivante illustre une application typique avec le bus I2C :

Figure 18 : Application avec un bus I2C.

Source : Philips, The I2C-Bus Specification, V2.1, Jan 2000

V-E-2 - Protocole

Le protocole du bus est défini de la manière suivante :

Figure 19 : Le maître (émetteur) adresse l'esclave (récepteur) avec une adresse 7 bits et envoie 2 octets de données.

Source : Philips, The I2C-Bus Specification, V2.1, Jan 2000

Le transfert de données entre le maître et l'esclave s'effectue de la manière suivante :

1. Le maître démarre le transfert des données en envoyant le bit de démarrage. Tous les esclaves entrent ainsi dans un état passif d'écoute.

2. Le maître envoie ensuite l'adresse de l'esclave avec lequel il aimerait communiquer. Cette adresse est composée de 7 bits.

3. Le maître envoie le bit R/W, qui fixe le sens de la communication : écriture depuis le maître à l'esclave ou lecture depuis l'esclave au maître.

4. Le protocole I_2C exige des confirmations (acknowledge) après chaque transmission d'un octet. L'esclave accrédite avec le premier bit de confirmation qu'il est prêt pour la communication.

5. Le transfert de données a lieu entre le maître et l'esclave. Un bit de confirmation est également échangé ici après chaque transmission d'un octet. Ce bit est fourni par le maître dans le mode lecture et par l'esclave dans le mode écriture.

6. Le transfert se termine avec un bit d'arrêt.

Le nombre d'octets à transmettre et le sens de la transmission varie en fonction des composants périphériques.

Ces informations sont fournies en général par la documentation de ces derniers. Le maître n'écrit par exemple qu'un octet dans un convertisseur D/A. Alors qu'avec une EEPROM, qui possède un espace d'adressage interne, le maître doit d'abord envoyer avec le champ « Offset » l'adresse de la case mémoire à accéder. Le transfert de données n'est effectué qu'ensuite jusqu'au bit Arrêt.

Le tableau suivant contient quelques exemples de transmissions (le bit de confirmation n'y est pas affiché) :

Tableau 2 : Exemple de protocole I2C

Composant

Protocole

Convertisseur D/A

Démarrage

Adresse de l'esclave

Wr

Données

Arrêt

Ecriture RAM

Démarrage

Adresse de l'esclave

Wr

Données

Données

Lecture RAM

Démarrage

Adresse de l'esclave

Wr

Données

Arrêt

Démarrage

Adresse de l'esclave

Rd

Données

Arrêt

Le protocole I_2C est défini au niveau bit de la manière suivante :

Niveau de pause :

- Le niveau de pause est high.

Condition de démarrage :

- La condition de démarrage est définie par un flanc descendant sur SDA, suivi d'un flanc descendant sur SCL. Ce signal est univoque et n'apparaît pas durant une transmission normale.

Condition d'arrêt:

- La condition d'arrêt est définie par un flanc montant sur SCL, suivi d'un flanc montant sur SDA. Ce signal est également univoque et n'apparaît pas durant une transmission normale.

Figure 20 : Condition de démarrage et d'arrêt

Les bits de données :

- La valeur du signal SDA est fixée en fonction du bit à transmettre : 0 = low, 1 = high. La validité est signalée avec un flanc montant du signal SCL. La lecture et l'écriture des bits de données ont toujours lieu pendant que le signal SCL est haut.

ACK :

- Le bit de confirmation (Acknowledge) veut dire que la réception est bonne ou que le récepteur est prêt pour des transmissions supplémentaires. Du point de vue du maître, l'esclave doit confirmer chaque octet écrit. Du point de vue de l'esclave, le maître doit confirmer chaque octet lu. Si ces confirmations n'ont pas lieu, le processus de transmission sera rompu avec le bit d'arrêt.

NACK :

- Not Acknowledged signale une erreur de transmission ou la fin de la disposition à recevoir.

Figure 21 : Transmission au niveau bit

Dans la Figure 21, le maître envoie en premier la condition de démarrage, ensuite il envoie l'adresse de l'esclave (1001011) et pour finir le bit R/W. L'esclave doit signaler sa présence avec le bit de confirmation. Il est important que le maître maintienne le SDA à un état de haute impédance durant cette phase. Les données peuvent être transmises après le bit de confirmation (cela n'est pas montré dans la figure). La communication est interrompue avec le bit d'arrêt.

Admettons que l'on veuille connecter un émetteur à plusieurs participants avec un bus I2C. Dans ce cas, tous les participants doivent se mettre dans un état de haute impédance. Certains microcontrôleurs peuvent configurer leurs sorties en tri state. Si cela n'est pas possible, il faut configurer les pins SCL et SDA en entrée.

V-E-3 - Implémentation

Certains microcontrôleurs possèdent une interface I2C intégrée. La programmation du composant périphérique se réalise alors avec des registres.

Toutefois, dans certains cas, la réalisation de l'interface I_2C ne peut s'effectuer qu'à l'aide de deux ports d'entrée/sortie numériques et du code. Cette réalisation peut néanmoins varier selon le type de contrôleur. Dans ce cas, on commence idéalement par la définition des ports SDA et SCL ainsi que l'énumération des différents codes d'erreurs de la manière suivante :

#define SDA P4_2
#define SCL P4_1
typedef enum {I2C_OK, I2C_ACK_ERROR, I2C_BUS_ERROR} I2C_Error;

Les fonctions I2C_Init(), I2C_Start(), I2C_Stop(), I2C_Write() et I2C_Read() sont alors implémentées par la suite.

Ces fonctions doivent pourvoir accéder aux ports SCA et SCL. Il est également recommandé de retourner des messages d'erreurs en cas de problème.

Exemple de code pour la fonction I2C_Start():

I2C_Error I2C_Start(void) {
I2C_Error error = I2C_OK;
SDA = 1; // Initial state
SCL = 1;
if (!SDA||!SCL){ // Test the bus levels
error = I2C_BUS_ERROR;
}
else {
SDA=0; // Start condition
... // Insert eventually a delay
SCL=0;
}
return(error);
}

V-F - Timer

V-F-1 - Introduction

La plupart des microcontrôleurs possèdent aujourd'hui un ou plusieurs « Timer/Counter » (français : minuterie/compteur) intégrés. Des fonctionnalités supplémentaires, comme celle de « Compare/Capture », permettent d'élargir encore plus les applications de ces contrôleurs. Ce chapitre donne un aperçu général sur ce sujet.

Les applications du Timer/Counter sont les suivantes :

- génération de fréquence d'horloge (ex. débit en Baud pour la communication sérielle) ;

- compteur d'événements externes ;

- génération de signaux PWM (ou convertisseur D/A) ;

- mesure du temps.

V-F-2 - Timer/Counter

En général, des registres de 8 ou 16 bits sont utilisés dans les modules Timer/Counter. Dans le mode Timer, ces registres sont incrémentés périodiquement à l'aide de l'horloge interne du système. Dans le mode Counter, cette incrémentation s'effectue en fonction de signaux externes.

Figure 22 : Timer/Counter Mode

Lorsqu'il y a dépassement de capacité (c'est-à-dire au passage de 0xFFFF à 0x0000 avec un registre de 16 bits), le drapeau (flag) d'overflow est mis à un. Ce dernier peut être observé à l'aide d'une boucle software (polling) ou il peut générer une interruption.

Remarque : le contenu du registre Timer est décrémenté avec certains contrôleurs.

V-F-3 - Timer avec recharge automatique

L'option « d'autorechargement » (anglais : auto reload) du Timer permet de générer une fréquence d'horloge avec une périodicité prédéfinie.

Le mode d'autorechargement permet d'initialiser également le registre du Timer avec une valeur prédéfinie.

Cette dernière est stockée dans le « registre de rechargement » (anglais : reload register). Ce mécanisme permet de générer des fréquences d'horloge de périodicité variable.

Figure 23 : Timer dans le mode auto chargement

V-F-4 - Unité comparatrice

L'unité comparatrice (anglais : Compare Unit) permet de comparer la valeur du registre compteur (Timer Register) avec celle du registre comparateur (anglais : Comparator Register).

Figure 24 : Unité comparatrice

L'unité comparatrice permet ainsi de générer des signaux PWM sans demander de la puissance de calcul au CPU. Le signal de sortie du comparateur et l'indicateur de dépassement (anglais : overflow flag) commutent un port de sortie numérique. Le latch est réinitialisé lorsqu'il y a dépassement de capacité du registre compteur, ce qui met le port au niveau logique 0. Ce même latch est mis à 1 lorsque le compteur du Timer atteint la valeur du registre comparateur, ce qui met le port au niveau logique 1. Ce mécanisme est illustré dans la figure suivante :

Figure 25 : Comportement du port de sortie dans le mode Compare

La Figure 25 se base sur un compteur qui compte vers le haut en mode Count-Up. Une variante ici est un compteur qui compte vers le bas en mode Count-Down. La valeur du registre compteur est alors comparée avec la valeur nulle. À chaque passage par zéro, le registre compteur est alors initialisé avec une valeur de rechargement prédéfinie.

V-F-5 - Entité de capture

En mode capture, un signal de déclenchement (anglais : trigger) provoque un transfert de la valeur du registre compteur (Timer Register) dans le registre de capture (Capture Register). Le signal de déclenchement peut être fourni soit par la partie logicielle (SW trigger) soit par la partie matérielle (HW trigger).

Figure 26 : Entité de capture

V-G - CAN

CAN (Controller Area Network) a été développé en 1981 par la maison Robert Bosch et Intel. Il a été prévu pour l'industrie automobile uniquement. Actuellement, CAN s'est également établi dans l'automation industrielle.

Grâce au grand nombre de chips CAN produits, l'accès au bus CAN est devenu relativement bon marché. On peut les trouver aussi bien comme éléments périphériques externes sur des systèmes à microprocesseurs ou comme modules internes à un microcontrôleur.

L'organisation d'utilisateurs CiA (CAN in Automation) a défini plusieurs standards pour son application dans le domaine de « l'automation industrielle ». Ce qui a conduit à un bon accueil du bus CAN. Le site suivant peut être consulté à titre d'information : www.can-cia.org.

Avec quelques restrictions, CAN peut être utilisé pour des applications temps réel. Les temps de réaction sont garantis pour des messages avec un ID de haute priorité.

VI - Références

Tableau 3 : Littérature

Nom

Auteur

Référence

MISRA-C:2004 (Guidelines for the use of the C
language in critical systems)
ISBN 978-0-9524156-2-6

Misra

[MISRA-C]

The Firmware Handbook
Jack Ganssle
ISBN 978-0-7506-7606-9

Newness

[Firmware_Handbook]

Embedded Software, The Works
Colin Walls
ISBN 0-7506-7954-9

Newness

[]

VII - Remerciements Developpez

L'équipe de rédaction Systèmes embarqués tient à remercier Dr Elham Firouzi pour ce tutoriel. Merci également à F-leb et à Winjerome pour leurs grands efforts fournis dans la gabarisation et la relecture.