Dans un programme simple à microprocesseur, il est facile de contrôler le code à l'aide des variables comme indicateurs et de prendre des décisions en utilisant des conditions if...then cela fonctionne bien tanque le système à contrôler est trivial. Mais lorsque le système devient de plus en plus complexe, ce type de programmation atteint ses limites.

Il existe des boîtes à outils qui sont là pour aider à la conception de ce type de programmes: pour l'exemple, les machines d'états.

Comment programmer un microcontrôleur PIC à l'aide d'une machine d'état?

Pour donner quelques éléments de réponse à cette question, on abordera les points suivants:

 

  • Présentation d’une machine à états ;
  • Les étapes qu’on va suivre pour traduire un problème sous forme de machine d’état ;
  • Un exemple de réalisation ;
  • Le test du montage sur palque à essais.

C’est quoi une machine d’états ?

On peut dire qu’une machine d’états est une représentation graphique du comportement d’un système. Une machine d’états lit un ensemble d’entrées, puis passe à un autre état en fonction de ces entrées.

Un état est une description de l’état d’un système qui attend qu’une transition soit exécutée.

Avec une machine d'état, on peut traduire des problèmes de code de logiciels embarqués complexes en blocs de codes simples et faciles à gérer.

Types de machines d’états

Il existe deux types de machines d’états :

  • La machine de Mealy : elle change ses actions en fonction de l’état courant et des entrées courantes ;
  • La machine de Moore : elle change ses actions uniquement en fonction de l’état courant.

La représentation d’une machine d’état, se traduit par un graphe appelé diagramme d’état.

Le dessin du diagramme d’état permet de se concentrer sur le problème à résoudre et non de penser déjà :

  • aux variables,
  • à la syntaxe du code qu’on va utiliser,
  • au compilateur qu’on va employer,
  • ou encore à la cible qu’on va devoir programmer

Au lieu de se concentrer en même temps sur les points cités précédemment, on peut juste se concentrer sur ce qu’on veut que le système fasse.

Toute fois il faudra avoir une idée sur le processeur qui sera utilisé, sera-t-il performant pour la tâche qu’il devra accomplir ?  Le nombre et le type d’entrées-sorties nécessaires sera-t-il suffisant ?

Etapes de conception d' un programme à l'aide d'une machine d'état

Etape 1 : rédaction d'une description de premier niveau du système ;

Etape 2 : rédaction des spécifications détaillées du système ;

Etape 3 : dessin du diagramme d’état;

Etape 4 : traduction du diagramme d’état sous sa forme algorithmique en blocks de code simples.

 

A la fin de ce processus, on peut réaliser le programme en langage de haut niveau, puis programmer la cible choisie.

Ces étapes, si elles sont respectées, peuvent  permettre de pouvoir documenter facilement le programme final, l’intérêt à ceci sera d’assurer une "maintenabilité" aisée du code par une autre personne autre que le concepteur du programme.

Exemple de réalisation

Contrôle de l’intensité lumineuse d’une LED.

Au lieu d’allumer ou d’éteindre simplement une LED à l’aide d’un bouton poussoir, on va aussi à partir du même bouton poussoir faire varier sa luminosité.

Matériel utilisé

            • un PIC16F88;
            • une LED;
            • un bouton poussoir;
            • une résistance de 330 Ω;
            • une résistance de 10 KΩ;
            • une plaque à essais;
            • une alimentation 5V.

Notre montage d'exemple sera appelé: "Contrôle d'intensité lumineuse de la LED".

 

Afin de décrire le problème qui nous est posé, on va utiliser une machine à états. Celle-ci nous permettra de penser uniquement au problème, et de le partitionner en petits morceaux. A l'aide du graphe d’état qu'on obtiendra, on peut aisément voir comment le système fonctionnera sans avoir besoin de lire des lignes de code complexes.

 

Etape1 :  Contrôle d’intensité lumineuse de la LED - description générale

Contrôle d’intensité lumineuse de la LED fera varier la luminosité de la LED, puis l’allumera et l’éteindra avec un seul bouton poussoir.

Cette description qui est encore générale, a besoin d'être fractionnée sous forme de briques détaillées.

Etape2 :  Contrôle d’intensité lumineuse de la LED - spécifications détaillées

Contrôle d’intensité lumineuse :

      • Un appui court sur le bouton poussoir fera varier l’intensité lumineuse de la LED d’un pas sur 256 niveaux d’intensité possibles.
      • Lorsque l’intensité atteint son niveau maximal, le niveau suivant devra réduire la luminosité de la LED.
      • Lorsque l’intensité atteindra son niveau minimal, le niveau suivant devra augmenter la luminosité de la LED.

Allumage de la LED :

      • Lorsque la LED est éteinte, tout appui long sur le bouton poussoir se traduira par l’allumage de la LED à son niveau d’intensité lumineuse qu’elle avait avant de l’éteindre.

Extinction de la LED :

      • Lorsque la LED est allumée, tout appui long sur le bouton poussoir, enregistrera le niveau d’intensité actuel dans une mémoire, puis éteindra la LED.

Etape 3 : Dessin du diagramme d’état

Le diagramme d’état qu’on va dessiner ici est basé sur le modèle de la machine de Mealy.

Dans une machine d’état, il existe une variable de contrôle, appelée variable d’état. Ceci n’est qu’une variable normale sauf qu’elle représente l’état courant de la machine d’états (ou alors la partie active du code correspondant).  Chaque changement de valeur de la variable d’état dirigent la machine d’états, d’un état vers un autre.

Avant de réaliser le diagramme d’état, on va d’abord définir les actions que l’utilisateur du système devra accomplir, en utilisant les spécifications de l’étape 2.

L’utilisateur effectuera 2 actions :

Un appui court  (APPUI_COURT);

Un appui long (APPUI_LONG) ;

Le diagramme d'état aura donc en tout et pour tout, trois actions:

REPOS;

APPUI_COURT;

APPUI_LONG.

Le système devra donc naviguer entre ces trois états. De façon simplifié voici à quoi ce diagramme ressemblera:

 

diagramme etat 1

 

La direction des flèches dans le diagramme indiquent le sens du flux du programme pour passer d'un état à un autre. Et dans le programme, les conditions seront à l'image de ces lignes conceptuelles.

Le passage d'un état à un autre sera ainsi matérialisé par le couple "Condition / Action", comme cela est illustré par la figure ci-après:

 

diagramme etat 2

 

La Condition comprendra toutes les informations nécessaires pour décider ce qu’il faut faire et devra être vérifiée pour que le programme passe à l’état suivant.

Les spécifications détaillées de l'étape 2, ont permi d'enrichir le diagramme précédent pour avoir un diagramme d'état plus détaillé, image du fonctionnement du système:

 

diagramme etat 3

 

On peut à présent afiner certaines actions qui vont être exécutées par le microcontrôleur, notamment en mettant en lumière les périphériques qui seront utilisés.

Dans ce cas de figure, le niveau de luminosité sera stocké dans une EEPROM ;  et la luminosité pourra varier grâce à la valeur qu’on envoie à un périphérique appelé PWM interne au composant afin que celui-ci change la largeur des impulsions sur la broche de sortie prévue à cet effet.  On obtient le diagramme amélioré ci-après:

 

diagramme etat 4

 

On peut donc à présent traduire le diagramme d’état sous-forme de pseudo-code, cette étape nous permet de penser à présent au programme qu’on va réaliser et non au langage de programmation qui sera utilisé.

La variable d'état ici ser appelée "Etat"

 

{variables / Etat:variable d'état; Bouton; appui_court; appui_long; Sens; montant; descendant; LED}

TANTQUE (Vrai)  /*on a ici une boucle infinie*/

     SELON (Etat)  FAIRE

          CAS  REPOS :

                LIRE Bouton ;

                SI  (Bouton = appui_court) ALORS Etat = APPUI_COURT ;

                SI (Bouton = appui long) ALORS Etat = APPUI_LONG ;

          CAS APPUI_COURT :

                SI (Sens = descendant) ALORS Diminuer valeur PWM ;

                SI (Sens = montant) ALORS Augmenter valeur PWM ;

                Vérifier la limite et ajuster Valeur PWM

                Etat = REPOS ;

          CAS APPUI_LONG :

                SI LED allumée ALORS

                     Enregistrer niveau de luminosité dans l’EEPROM ;

                     Eteindre LED ;

                SINON

                     Récupérer niveau de luminosité enregistré dans l’EEPROM ;

                     Allumer LED, avec la valeur stockée dans l’EEPROM ;

                FIN SI

                Etat = REPOS ;

     FIN SELON

FIN TANQUE

 

Le programme

Comment traduire les états d'une machine d'état en langage C ?

Pour déclarer la variable d'état en langage C, on va utiliser le type Enuméré, ce type de déclaration permet de travailler directement avec les noms des états.

Par exemple la variable d'état peut être déclarée de la manière suivante:

enum MachineEtat {REPOS, APPUI_COURT, APPUI_LONG};

Nul besoin ainsi de se rappeler que 0 représente REPOS, 1 représente APPUI_COURT, ou que 2 représente APPUI_LONG.

Lorsqu'on va définir la variable qui utilise le type MachineEtat, on peut directement lui assigner les valeurs du type énuméré.

Par exemple si je veux initialiser la variable MachineEtat à REPOS, il suffit d'écrire:

enum MachineEtat Etat = REPOS;

Pour assigner d'autres valeurs il suffit d'écrire:

Etat = APPUI_LONG;  //on a assigné ici à la varaible Etat, la valeur APPUI_LONG

Le pseudo-code proposé dans les lignes précédente donne déjà un idée de ce que sera le programme final, sachant certaines actions comme par exemple "Récupérer niveau de luminosité enregistré dans l’EEPROM", constituent des fonctions à part entière, qui seront appelées par le programme principal.

Ci-après tous les fichiers sources du programme; par souci de cohérence par rapport au langage de programmation, j'ai preféré ecrire les variables ainsi que les commentaires en anglais.

 

Entêtes et fichiers .c:

 /* 
 * File:  deviceIO header 
 * Author: JtBB
 * Comments:
 * Revision history: 1.0
 */

// This is a guard condition so that contents of this file are not included
// more than once.  
#ifndef __DEVICEIO_H
#define	__DEVICEIO_H
  
// TODO Insert appropriate #include <>
#define LED RB3 //Lamp on port B pin 3 (3))
#define pushButton RB0 //push button on port B pin 0 (1))
#define LED_dir TRISB3
#define pushButton_dir TRISB0
#define In 1   //port as input
#define Out 0  //port as output
#define KEY_NONE 0 //no button hit
// TODO Insert C++ class definitions if appropriate

// TODO Insert declarations

// TODO Insert declarations or function prototypes (right here) to leverage 
// live documentation


    // TODO If C++ is being used, regular C code needs function names to have C 
    // linkage so the functions can be used by the c code. 

#endif	

 
 /* 
 * File: EEPROM read / write header  
 * Author: Author JtBB
 * Comments:
 * Revision history: 1.0
 */

// This is a guard condition so that contents of this file are not included
// more than once.  
#ifndef __EEPROM_H
#define	__EEPROM_H


// TODO Insert declarations or function prototypes (right here) to leverage 
// live documentation
void eeprom_write(unsigned char eepromAddress, unsigned char eepromData);
unsigned char eeprom_read(unsigned char eepromAddress);

#endif	

 
 /* 
 * File:   Pwm header
 * Author: JtBB
 * Comments:
 * Revision history: 1.0
 */

// This is a guard condition so that contents of this file are not included
// more than once.  
#ifndef __PWM_H
#define	__PWM_H


// TODO Insert declarations or function prototypes (right here) to leverage 
void pwm_init(void);
void pwm_output_to_pin(unsigned int);

// TODO If C++ is being used, regular C code needs function names to have C 
// linkage so the functions can be used by the c code. 


#endif	

 /* 
 * File:  includes header 
 * Author: JtBB
 * Comments:
 * Revision history: 1.0
 */

// This is a guard condition so that contents of this file are not included
// more than once.  
#ifndef __INCLUDES_H
#define	__INCLUDES_H
// Device configuration bit
// CONFIG1
#pragma config FOSC = INTOSCIO       // Oscillator Selection bits (HS oscillator); 
#pragma config WDTE = OFF       // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (PWRT disabled)
#pragma config MCLRE = ON      // RA5/MCLR/VPP Pin Function Select bit (RA5/MCLR/VPP pin function is digital I/O, MCLR internally tied to VDD)
#pragma config BOREN = ON       // Brown-out Reset Enable bit (BOR enabled)
#pragma config LVP = OFF        // Low-Voltage Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming)
#pragma config CPD = OFF        // Data EE Memory Code Protection bit (Code protection off)
#pragma config WRT = OFF        // Flash Program Memory Write Enable bits (Write protection off)
#pragma config CCPMX = RB3      // CCP1 Pin Selection bit (CCP1 function on RB0)
#pragma config CP = OFF         // Flash Program Memory Code Protection bit (Code protection off)

// CONFIG2
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config IESO = OFF       // Internal External Switchover bit (Internal External Switchover mode disabled)

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.
#include  // include processor files - each processor file is guarded.  
#include "deviceIO.h"
#include "pwm.h"
#include "eeprom.h"
// TODO Insert appropriate #include <>
// TODO Insert declarations
#define _XTAL_FREQ  4000000  //cristal oscillator in 4 MHz 
#define PWM_freq 4
#define KEY_NULL 0
#define KEY_TAP  1
#define KEY_LP   2

// TODO Insert declarations or function prototypes (right here) to leverage 
// live documentation
void init_port(void);
void enable_interrupts(void);
//void disable_interrupts(void);
unsigned char get_button_press(void);
void check_pwm_value_and_update_it(void);
int show_status(void);
void do_state_machine(void);
// 
// TODO If C++ is being used, regular C code needs function names to have C 
// linkage so the functions can be used by the c code. 

#endif	

 /*
 * File:   pwm.c
 * Author: JtBB
 *
 * Created on 10 mars 2022, 17:08
 */
// In this function, CCP1 module is use to generate PWM on RB0 pin;
// Timer 2 module is also use for this porpose;
// PWM resolution = 10 bits

#include "includes.h"
//initialize pwm 
void pwm_init(void){
    PR2 = 255; // step 1 as per pic data sheet.
    CCP1CON = 0x0C; // step 2.
    LED_dir = 0; // step 3 make ccp1 pin as output
    T2CON = 0x00; // step 4 set prescaler value to 1 (=0x00), frequency 3,9KHz.
    T2CON |= 0x04; //step 5 enable Timer 2 with PWM
    //for other config see includes header.
}
void pwm_output_to_pin(unsigned int duty_cycle){ 
   
       CCP1CON &= 0x0F; //Clear CCPICON bits<5:4>, and then write duty_cycle LSB 
       CCP1CONbits.CCP1X = duty_cycle & 1;
       CCP1CONbits.CCP1Y = duty_cycle & 2;
       CCPR1L  = duty_cycle >> 2;//write 8 bits msb into CCPR1L
   
}
 
 /*
 * File:   eeprom.c
 * Author: JtBB
 *
 * Created on 9 mai 2022, 16:26 V1.0
 */

#include "includes.h"

//write operation to eeprom
//vers v1.0
void eeprom_write(unsigned char eepromAddress, unsigned char eepromData) {
    while(EECON1bits.WR); //wait here if eeprom writting is not finish
    EEADR = eepromAddress; //write this addres to eeprom register
    EEDATA = eepromData; //Load data to be written
    EECON1bits.WREN = 1; 
    INTCONbits.GIE = 0; //disablle all interrupts
    EECON2 = 0x55; //write this special sequences see datasheet
    EECON2 = 0xAA; //see datasheet
    EECON1bits.WR = 1; //set this bit to trigger eeprom write operation
    INTCONbits.GIE = 1; //enable all interrupts
    EECON1bits.WREN = 0; // disable program operation
}

//read operation from eeprom

unsigned char eeprom_read(unsigned char eepromAddress){
    while(EECON1bits.RD || EECON1bits.WR); //check if read or write operation is pending
    EEADR = eepromAddress; //write the address to read data from
    EECON1bits.RD = 1; //set to perform read operation
    return(EEDATA);
}

 /*
 * File:   ledbulb_main.c
 * Author: JtBB
 *
 * Created on 9 mars 2022, 11:46
 * Retease: 1.0 Inintial
 */
#include "includes.h"

// file type
enum state_machine {IDLE, SHORT_PRESS, LONG_PRESS};
//some vairable used in this scope
static unsigned char pwr_on = 0;
unsigned int pwm_val = 0; //char in the example given 0 static
unsigned int counter = 0;
static char light_dir = 1;
unsigned char button_hit = 0;
//initialise port 
void init_port(void){
    LED = 0;
    LED_dir = Out; //led is connected here
    pushButton = 0;
    pushButton_dir = In; //push button is an input
}
//interrupt service routine for portB pin RB0 rising edge
void __interrupt() bulb_isr(void){
    //if(INTCONbits.INT0IE == 1 && INTCONbits.INT0IF == 1){
    button_hit = 1;
       NOP();
       NOP();
       INT0IF = 0;
    //}
}
//enable interrupts
void enable_interrupts(void){
    INTCONbits.GIE = 1; //enable all interrupts
    INTCONbits.PEIE = 1;
    INTCONbits.INT0IE = 1; //enable INT0 interrupt
    OPTION_REGbits.INTEDG = 1; //on rising edge
}

//we want to get the button press
unsigned char get_button_press(void){
    if (button_hit){
        button_hit = 0;
        do{
            __delay_ms(50);
            counter++;
        }while(pushButton);
    }
    //now map buttons
    if (counter == 0) return KEY_NULL;
    if (counter <= 10){
        counter = 0;
        return KEY_TAP;
    }
    if (counter > 50){//greater or equal to 2.5 sec 
        counter = 0;
        return KEY_LP;
    }
}


//check pwm limit and adjust when able with step = 4
void check_pwm_value_and_update_it(void){//
    if (pwm_val > 1023){
        light_dir = -1; pwm_val = 1023; //
    }
    if (pwm_val < 4){
        light_dir = 1; pwm_val = 4;
    }   
}

//state machine routine
void do_state_machine(void){
    //state machine variable first
    enum state_machine state = IDLE;
    while(1){//
        switch(state){
            case IDLE:
                switch(get_button_press()){
                    case KEY_TAP://(2)
                    if(pwr_on){
                        state = SHORT_PRESS;
                    }else
                    {
                        state = IDLE;
                    }
                    break;
                    case KEY_LP://(3)
                        state = LONG_PRESS;
                    break;
                }
            break;
            
            case SHORT_PRESS:
                
                if(light_dir == 1){
                    pwm_val +=4; 
                }else 
                   {                
                    pwm_val -=4; 
                }
                check_pwm_value_and_update_it();
                pwm_init();
                pwm_output_to_pin(pwm_val);   
                state = IDLE;
                break;
                
            case LONG_PRESS:
                if (pwr_on == 0){//Then read eeprom to get pwm value
                    pwm_val = (int)eeprom_read(0);
                    pwr_on = 1;
                    pwm_init();
                    pwm_output_to_pin(pwm_val);
                }else
                {//else store pwm value
                    eeprom_write(0, pwm_val);
                    pwr_on = 0;
                    pwm_val = 0; //
                    pwm_init();
                    pwm_output_to_pin(pwm_val);
                }
                state = IDLE;
            break;
        }//end switch
    }//end while
}//end do state machine
//the main program 
void main(void) {
    OSCCONbits.IRCF = 0B110; //to define 4MHz internal clock 
    init_port();
    enable_interrupts();
    __delay_ms(20);
    do_state_machine();
    return;
}

 

 

 

Le schéma du montage a été réalisé sur plaque à essai, il est vraiment réduit à son expression la plus simple: (voir vidéo).

 

Après avoir programmé le microcontrôleur PIC16F88, on a câblé le tout sur palque à essais. On peut maintenant voir ce montage à l'oeuvre:

 

 

A partir de cet exemple, on a montré qu'on pouvait créer une interface de commande à un bouton poussoir, à l'aide des machines d'états. Pourquoi ne pas envisager utiliser un composant à 8 broches si celui-ci dispose des périphériques nécessaires. En y ajoutant un peu plus de puissance, ce montage pourrait par exemple servir à la commande de vitesse d'un moteur à courant continu, d'une petite perçeuse.