mardi 19 février 2013

Afficheur Alphanumérique (partie 2)




Architecture Logicielle


L'architecture logicielle de la carte contrôleur est relativement simple. 
On reçoit des trames provenant du bus I²C via le handler d'interruption et les empiler dans une file d'attente (FIFO), ensuite, périodiquement, on dépile la file d'attente et traite les trames enregistrées pour afficher les informations sur l'écran LCD.





Voici les sources du contrôleur PIC16f876A, versionnées sous Github :


Dialogue avec le bus I²C


Initialisation


Le micro-contrôleur doit être initialisé en tant qu'esclave sur le bus I²C, à une adresse particulière, ici, 0x76.
D'après la datasheet du micro, de nombreux registres (cinq pour être exact) doivent être configurés pour faire fonctionner l'I²C.
  • MSSP Control Register (SSPCON) - Registre de configuration et de controle
  • MSSP Control Register 2 (SSPCON2) - Registre de configuration et de contrôle
  • MSSP Status Register (SSPSTAT) - Registre d'états
  • Serial Receive/Transmit Buffer Register (SSPBUF) - Buffer de réception et transmission de données
  • MSSP Address Register (SSPADD) - Registre d'adresse de l'esclave

#define PIC16F876A_I2C_SLAVE_ADDR       0x76
#define SCL                             RC3
#define SDA                             RC4

int8_t i2c_init(void) {
        // Synchronous Serial Port Disable bit
        SSPEN = 0;
        // Set RC3 as Input
        TRISC3 = 1;
        // Set RC4 as Input
        TRISC4 = 1;
                        
        // Synchronous Serial Port Mode Select bit
        // 0110 => I2C Slave mode, 7-bit address
        SSPM3 = 0;
        SSPM2 = 1;
        SSPM1 = 1;
        SSPM0 = 0;

        // Set the Slave address
        SSPADD = PIC16F876A_I2C_SLAVE_ADDR << 1;  // 7 bits address = Bits 7654 321
        
        // CKP: SCK Release Control bit
        CKP = 1;
        
        // Synchronous Serial Port Enable bit
        SSPEN = 1;
        
        SSPCON2 = 0x01;
                
        //SMP: Slew Rate Control bit
        SMP = 1;

        //enable I²C interrupt
        SSPIE = 1;
        
        // clear SSPIF interrupt flag
        SSPIF = 0;
                
        return RET_OK;
}

Fonctionnement


L'envoi et la réception d'une trame I²C correspond à un schéma type de base :

  • Bit de START
  • Octet d'adresse + bit de lecture ou d'écriture
  • Octet(s) de données
  • Bit de STOP



Les différents bits du registre de status permettent de renseigner le programme sur le type d'opération en cours :
  1. Opération d'écriture du maître (lecture de l'esclave), le dernier octet reçu est une adresse.
    1. Bit 0 : Buffer full bit = 1
    2. Bit 2 : Read or Write bit = 0
    3. Bit 3 : Start bit = 1
    4. Bit 5 : Data or Address bit = 0
  2. Opération d'écriture du maître (lecture de l'esclave), le dernier octet reçu est une donnée.
    1. Bit 0 : Buffer full bit = 1
    2. Bit 2 : Read or Write bit = 0
    3. Bit 3 : Start bit = 1
    4. Bit 5 : Data or Address bit = 1
  3. Opération de lecture du maître (écriture de l'esclave), le dernier octet reçu est une adresse.
    1. Bit 0 : Buffer full bit = 0
    2. Bit 2 : Read or Write bit = 1
    3. Bit 3 : Start bit = 1
    4. Bit 5 : Data or Address bit = 0
  4. Opération de lecture du maître (écriture de l'esclave), le dernier octet reçu est une donnée.
    1. Bit 0 : Buffer full bit = 0
    2. Bit 2 : Read or Write bit = 1
    3. Bit 3 : Start bit = 1
    4. Bit 5 : Data or Address bit = 1

J'ai opté pour un fonctionnement du bus I²C en interruption, ce qui signifie que dès qu'il y a une activité sur le bus, le programme entre dans la routine d'interruption, regarde le type d'opération en cours et enregistre les données dans la file d'attente, si nécessaire.


Dialogue avec l'écran LCD 2x16 caractères


Initialisation


Deux modes d'initialisation sont possibles, mode 4 bits et mode 8 bits. Pour économiser quelques entrées du micro, j'ai choisi le premier mode (4 bits).



L'organigramme est assez simple pour l'initialisation, On réalise une attente de 15 ms puis on met les broches DB5 et DB4 à l'état haut, tout en laissant les autres à l'état bas.
Il faut toujours envoyer une impulsion positive d'au moins 450 ns, après la mise à l'état haut.
On réalise ensuite, une attente de 4.1 ms, suivie de la mise à l'état haut des broches DB5 et DB4.
Comme précédemment, on fait une impulsion d'au moins 450 ns pour valider la commande.
On réalise, pour finir, une attente de 100 us, suivie de la mise à l'état haut des broches DB5 et DB4 puis une attente de validation.

int8_t lcd_init(void) {
        int8_t i8_ret = 0;
        
        ADCON1 = 0x06;
        // make the corresponding PORTA pin an output
        TRISA = 0;
        // clear all output pins
        PORTA = 0;
        
        // clear all three output pins
        LCD_RS = 0;
        LCD_EN = 0;
        
        // Wait for more than 15 ms after VCC rises to 4.5 V
        __delay_ms(15);
        // RS R/W DB7 DB6 DB5 DB4
        // 0   0   0   0   1   1
        LCD_D4 = 1;
        LCD_D5 = 1;
        LCD_D6 = 0;
        LCD_D7 = 0;
        // Il faut toujours envoyer une impulsion positive d'au moins 450ns, 
        // après la mise à l'état haut des broches DB5 et DB4, sur la broche EN.
        LCD_EN = 1; 
        NOP();
        LCD_EN = 0;
        // Wait for more than 4.1 ms
        __delay_ms(5);
        LCD_EN = 1;
        NOP(); 
        LCD_EN = 0;
        // Wait for more than 100 μs
        __delay_us(200);
        LCD_EN = 1;
        NOP(); 
        LCD_EN = 0;
        __delay_us(200);
        // RS R/W DB7 DB6 DB5 DB4 
        // 0   0   0   0   1   0  => interface four bits mode 
        LCD_D4 = 0;
        LCD_D5 = 1;
        LCD_D6 = 0;
        LCD_D7 = 0;
        
        LCD_EN = 1;
        NOP();
        LCD_EN = 0;

        __delay_ms(5);
        // => Set interface length
        // RS R/W DB7 DB6 DB5 DB4 
        // 0   0   0   0   1   0  
        // 0   0   N   F   *   * 
        lcd_write_byte(0x28);
        // => Display On, Cursor On, Cursor Blink Off
        // RS R/W DB7 DB6 DB5 DB4 
        // 0   0   0   0   0   0  
        // 0   0   1   0   0   0
        lcd_write_byte(0x08);
        // => Clear screen
        // RS R/W DB7 DB6 DB5 DB4 
        // 0   0   0   0   0   0  
        // 0   0   0   0   0   1
        lcd_write_byte(0x01);
        // => Set entry Mode
        // RS R/W DB7 DB6 DB5 DB4 
        // 0   0   0   0   0   0  
        // 0   0   0   1   D   S
        lcd_write_byte(0x06);
        // RS R/W DB7 DB6 DB5 DB4 
        // 0   0   0   0   0   0  
        // 0   0   1   D   C   B
        lcd_write_byte(0x0C);

        // erase display
        lcd_clear_display();

        return i8_ret;
}

la primitive d'écriture pour l'écran LCD est la suivante :

int8_t lcd_write_byte(const uint8_t /* in */ ui8_byte) {
        LCD_D4 = (ui8_byte >> 4) & 0x01;
        LCD_D5 = (ui8_byte >> 5) & 0x01;
        LCD_D6 = (ui8_byte >> 6) & 0x01;
        LCD_D7 = (ui8_byte >> 7) & 0x01;
        
        LCD_EN = 1; 
        NOP();
        LCD_EN = 0;
        if(LCD_RS == 1) {
                __delay_us(200);
        }
        else {
                __delay_ms(5);
        }
        LCD_D4 = ui8_byte & 0x01;
        LCD_D5 = (ui8_byte >> 1) & 0x01;
        LCD_D6 = (ui8_byte >> 2) & 0x01;
        LCD_D7 = (ui8_byte >> 3) & 0x01;
        
        LCD_EN = 1; 
        NOP();
        LCD_EN = 0;
        if(LCD_RS == 1) {
                __delay_us(200);
        }
        else {
                __delay_ms(5);
        }
        return RET_OK;
}

fonctionnement


De nombreuses commandes permettent de jouer avec l'écran (récapitulées dans ce tableau, tiré de la datasheet) :


toutes les commandes envoyées à l'écran sont basées sur la primitive d'écriture, exemples :

Effacer l'écran :

void lcd_clear_display(void) {
        LCD_RS = 0;
        lcd_write_byte(0x01);
}

Retourner au début de l'écran :

void lcd_return_home(void) {
        LCD_RS = 0;
        lcd_write_byte(0x02);
}

Ecrire un caractère sur l'écran :

void lcd_put_char(const char_t /* in */ i8_char) {
        LCD_RS = 1;     // write character
        lcd_write_byte(i8_char);
        LCD_RS = 0;
}

File d'attente : FiFo


J'ai mis en place une file d'attente statique de type FiFo (First In, First Out), ce qui signifie que les premières données empilées dans la mémoire seront les premières à être dépilées et une fois arrivé au bout de la file d'attente, on boucle sur le début de la file d'attente. Elle est statique car la mémoire d'un micro-contrôleur est limitée, donc on déclare une taille figée de FiFo (64 octets).


Concrètement, on a un tableau d'octets déclaré et deux pointeurs (un de lecture et un d'écriture) qui vont se balader sur ce tableau, avec comme contrainte que le pointeur de lecture ne dépasse jamais le pointeur d'écriture.

#define FIFO_MAX_SIZE   64              /*!< Buffer size max */
uint8_t gpui8_buffer[FIFO_MAX_SIZE];    /*!< Static buffer */

void fifo_init(void) {
        uint8_t ui8_idx = 0;
        // initialise buffer
        for(ui8_idx = 0; ui8_idx < FIFO_MAX_SIZE; ui8_idx ++) {
                gpui8_buffer[ui8_idx] = 0;
        }

        // initialise pointers
        gpui8_read_buf = &gpui8_buffer[0];
        gpui8_write_buf = &gpui8_buffer[0];
        gb_flag_cur_state_buf = BUFFER_STAT_EMPTY;
        return;
}

Messagerie I²C


Une messagerie simple a été conçue pour avoir accès aux fonctions principales de l'afficheur LCD et afficher la plupart des informations à l'écran.


Lien vers la première partie du tutoriel : Afficheur alphanumérique (Partie 1)
Lien vers la troisième partie du tutoriel : Afficheur alphanumérique (Partie 3)

Aucun commentaire:

Enregistrer un commentaire