Micro contrôleurs AVR/Le langage C pour AVR
Ce chapitre comporte beaucoup de redondance avec un autre projet Utiliser les PIC 16F et 18F et particulièrement avec le chapitre Introduction au langage C et ses exercices associés.
Arithmétique binaire et expressions en C AVR
modifierPour bien comprendre la signification des expressions, il est essentiel d’avoir 2 notions en tête : la priorité et l'associativité. Nous donnons ci-après un tableau des opérateurs par priorité décroissante :
Catégorie d'opérateurs
|
Opérateurs
|
Associativité
|
fonction, tableau, membre de structure, pointeur sur un membre de structure | ( ) [ ] . -> | Gauche → Droite |
opérateurs unaires | - ++ -- ! ~
* & sizeof (type) |
Droite ->Gauche |
multiplication, division, modulo | * / % | Gauche → Droite |
addition, soustraction | + - | Gauche → Droite |
décalage | << >> | Gauche → Droite |
opérateurs relationnels | < <= > >= | Gauche → Droite |
opérateurs de comparaison | == != | Gauche → Droite |
et binaire | & | Gauche → Droite |
ou exclusif binaire | ^ | Gauche → Droite |
ou binaire | ǀ | Gauche → Droite |
et logique | && | Gauche → Droite |
ou logique | ǁ | Gauche → Droite |
opérateur conditionnel | ? : | Droite → Gauche |
opérateurs d'affectation | = += -= *= /= %=
&= ^= |= <<= >>= |
Droite → Gauche |
opérateur virgule | , | Gauche → Droite |
La priorité des opérateurs va décroissante lorsqu'on se déplace du haut du tableau vers le bas du tableau. Quand les opérateurs ont même priorité, c’est la colonne de droite sur l’associativité qui est utilisée.
Une expression ne doit comporter ni trop ni trop peu de parenthèses sous peine de devenir illisible. Tout est donc affaire de dosage et c’est cet apprentissage qui est long. Il faut garder à l'esprit le fait que la taille du programme généré ne dépend pas du nombre de parenthèses.
Exercice 1
modifierEnlever les parenthèses des expressions suivantes lorsqu'elles peuvent être retirées.
a=(25*12)+b;
if ((a>4) &&(b==18)) { }
((a>=6)&&(b<18))||(c!=18)
c=(a=(b+10));
Évaluer toutes ces expressions pour a=6, b=18 et c=24
a = 25 * 12 + b;
if (a>4 && b==18) { } // impossible d'enlever les parenthèses restantes
a>=6 && b<18 || c!=18
c = a = b+10;
Evaluation :
- a reçoit 318
- l’expression booléenne dans le if est vraie
- l’expression complète est vraie car c est différent de 18
- b+10 = 28 sera affecté à a puis à c
Quelques types numériques
modifierLes types du C AVR sont[1] :
unsigned char a;//8 bits, 0 to 255
signed char b; //8 bits, -128 to 127
unsigned int c; //16 bits, 0 to 65535
signed int d; //16 bits, -32768 to 32767
long e; //32 bits, -2147483648 to 2147483647
float f; //32 bits
Nous vous recommandons d'utiliser plutôt les types prédéfinis du compilateur C (du GNU)
uint8_t a; //8 bits, 0 to 255
int8_t b; //8 bits, -128 to 127
uint16_t c; //16 bits, 0 to 65535
int16_t d; //16 bits, -32768 to 32767
int32_t e; //32 bits, -2147483648 to 2147483647
float f; //32 bits
Le gros avantage est que les tailles des types sont dans leurs noms :
- uint8_t est sur 8 bits
- uint32_t est sur 32 bits
Ainsi les types utilisés seront indépendants des cibles.
Travail sur les bits individuels en C
modifierLes explications de cette section peuvent être reprises sans modification du livre sur les PIC 16F : Travail sur les bits individuels en C. Pour éviter les allers et retours, nous allons reproduire cette section ici.
Lorsqu'on programme en C sur une petite architecture comme le ATMegaXXX on est très souvent amené à faire des tests sur les bits individuels de certains registres. On peut aussi être amené à être plus actif en positionnant certains bits individuels à 1 ou à 0.
Positionner un bit particulier en C
modifierLe principe de base est d’utiliser des masques.
On appelle masque une valeur définie qui va servir à l'aide d'un opérateur ou ou un opérateur et à positionner des bits individuels.
Les opérations logiques effectuées sont des opérations bit à bit :
- & et logique bit à bit en C pour positionner un ou plusieurs 0
- | ou logique bit à bit en C pour positionner un ou plusieurs 1
Les masques seront exprimés en hexadécimal. Ce n’est pas obligatoire car certains compilateurs offrent la possibilité d'écrire en binaire, mais si l’on veut être portable, il vaut mieux utiliser l'hexadécimal.
Utisation de masques
modifierComme déjà évoqué, le principe de base est d’utiliser un OU pour positionner à 1 et ET pour positionner à 0.
Un schéma de principe sera peut-être plus parlant :
Les masques sont donnés en rouge en binaire et en hexadécimal. Les X dans le registre représentent des valeurs que l’on ne connait pas. Remarquez que les X sont restés des X (ce qui signifie qu’ils n'ont pas changé) sauf pour un seul bit.
- Pour positionner un ou plusieurs bits à 1 on écrit un ou plusieurs des 1 aux places concernées et on complète par des 0. Ceci formera le masque binaire qu’il faudra transformer en hexadécimal et utiliser avec un opérateur OU.
- Pour positionner un ou plusieurs bits à 0 on écrit un ou plusieurs des 0 aux places concernées et on complète par des 1. Ceci formera le masque binaire qu’il faudra transformer en hexadécimal et utiliser avec un opérateur ET.
Utilisation de macros
modifierIl existe plusieurs autres méthodes pour positionner les bits (http://www.microchipc.com/HiTechCFAQ/index.php)
1° méthode pour positionner bit à bit :
#define bit_set(var,bitno) ((var) |= 1 << (bitno))
#define bit_clr(var,bitno) ((var) &= ~(1 << (bitno)))
unsigned char x=0b0001;
bit_set(x,3); //now x=0b1001;
bit_clr(x,0); //now x=0b1000;*/
2° méthode pour positionner plusieurs bits
#define bits_on(var,mask) var |= mask
#define bits_off(var,mask) var &= ~0 ^ mask
unsigned char x=0b1010;
bits_on(x,0b0001); //now x=0b1011
bits_off(x,0b0011); //now x=0b1000 */
Tests de bits en C
modifierUtilisation des masques
modifierOn utilise encore des masques mais en général toujours avec l'opérateur ET.
On utilise et masque avec un 1 en face du bit que l’on veut tester. On construit ensuite une expression booléenne pour tester le résultat en le comparant à une valeur prédéfinie.
Cette valeur prédéfinie est égale au masque lorsqu'on cherche à savoir si c’est un 1 ou à 0 partout dans le cas contraire. Le test de gauche dans la figure ci-dessus peut donc s'écrire :
// unsigned char Reg;
if ((Reg & 0x40) == 0x40) { //E.B. vraie si bit B6 à 1
...
}
tandis que le test de droite s'écrit quant à lui
// unsigned char Reg;
if ((Reg & 0x08) == 0x00) { //E.B. vraie si bit B3 à 0
...
}
Il n’est pas possible de supprimer les parenthèses dans les deux programmes ci-dessus.
Utilisation d'une macro
modifierLes tests d'un bit particulier en C peuvent aussi être réalisés de la manière suivante (http://www.microchipc.com/HiTechCFAQ/index.php)
x=0b1000; //decimal 8 or hexadecimal 0x8
if (testbit_on(x,3)) a(); else b(); //function a() gets executed
if (testbit_on(x,0)) a(); else b(); //function b() gets executed
if (!testbit_on(x,0)) b(); //function b() gets executed
#define testbit_on(data,bitno) ((data>>bitno)&0x01)
Utilisation de fonctions prédéfinies du compilateur avr-gcc
modifierNous avons réuni en annexe I un ensemble d'instructions disponibles avec notre compilateur C. Il vous faut bien distinguer la macro _BV de l'instruction sbi. La première peut mettre son résultat dans une variable (ou un Registre), tandis que la deuxième travaille sur un PORT ou un registre en général.
Exercice 2
modifierOn présente ci-dessous un registre (ou une case mémoire) de 8 bits avec la numérotation des bits associés.
b7
|
b6
|
b5
|
b4
|
b3
|
b2
|
b1
|
b0
|
1
|
0
|
1
|
1
|
0
|
0
|
0
|
1
|
Le bit b0 est le poids faible et du coup sera toujours dessiné à droite. Les valeurs dans les cases n'ont aucune importance pour la suite de l'exercice.
Si une variable p1 de type signed char (8 bits signés) est déclarée, écrire les expressions en C (utilisant des masques) permettant de :
- mettre à 1 le bit b2
- mettre à 1 le bit b3 et b6
- mettre à 0 le bit b0
- mettre à 0 le bit b4 et b5
- inverser le bit b3 (se fait facilement avec un ou exclusif)
- mettre à 1 le bit b2 et à 0 le bit b0
- mettre à 1 les bits b0 et b7 et à 0 les bits b3 et b4
signed char p1;
p1 = p1 | 0x04; // mettre à 1 le bit b2
p1 = p1 | 0x48; // mettre à 1 le bit b3 et b6
p1 = p1 & 0xFE; // mettre à 0 le bit b0
p1 = p1 & 0xCF; // mettre à 0 le bit b4 et b5
p1 = p1 ^ 0x08; // inverser le bit b3 (se fait facilement avec un ou exclusif)
p1 = p1 & 0xFE | 0x04 ; // mettre à 1 le bit b2 et à 0 le bit b0
p1 = p1 & 0xE7 | 0x81 ; // mettre à 1 les bits b0 et b7 et à 0 les bits b3 et b4
Exercice 3
modifierRefaire l'exercice 2 en utilisant la macro "_BV" du compilateur C.
signed char p1;
p1 = p1 | _BV(2); // mettre à 1 le bit b2
p1 = p1 | _BV(3) | _BV(6); // mettre à 1 le bit b3 et b6
p1 = p1 & ~_BV(0); // mettre à 0 le bit b0
p1 = p1 & ~(_BV(4)|_BV(5)); // mettre à 0 le bit b4 et b5
p1 = p1 ^ _BV(3); // inverser le bit b3 (se fait facilement avec un ou exclusif)
p1 = p1 & ~_BV(0) | _BV(2) ; // mettre à 1 le bit b2 et à 0 le bit b0
p1 = p1 & ~(_BV(3)|_BV(4)) | (_BV(0) | _BV(7)) ; // mettre à 1 les bits b0 et b7 et à 0 les bits b3 et b4
Utiliser les registres de l'AVR (ATMegaXXX) en C
modifierL'idéal, pour une écriture facile en langage C, serait de pouvoir écrire :
// affectation d'une valeur dans un registre :
PORTB = val;
qui pourrait mettre directement la valeur contenue dans la variable val dans un registre appelé PORTB. Pour cela, il faut que le compilateur C (gcc pour nous) connaisse le mot PORTB. En général, ce sera le cas si vous mettez l'entête
#include <avr/io.h>
en tout début de votre programme. Mais qu'en est-il pour les bits des registres ?
Le registre SREG présenté ci-dessus comporte des bits qui possèdent un nom. Ce n’est pas le seul registre à avoir cette propriété. Mais peut-on en déduire que les bits des registres possèdent tous un nom que le compilateur C connait ? La réponse est oui mais probablement pas à 100% !
Exemple de fichier d'en-tête pour gcc
modifierPour illustrer les questions de la section précédente de manière un peu plus concrète, voici un exemple partiel de fichier d'entête :
/* Port D */
#define PIND _SFR_IO8(0x10)
#define DDRD _SFR_IO8(0x11)
#define PORTD _SFR_IO8(0x12)
/* Port C */
#define PINC _SFR_IO8(0x13)
#define DDRC _SFR_IO8(0x14)
#define PORTC _SFR_IO8(0x15)
/* Port B */
#define PINB _SFR_IO8(0x16)
#define DDRB _SFR_IO8(0x17)
#define PORTB _SFR_IO8(0x18)
....
/* PORTB */
#define PB7 7
#define PB6 6
#define PB5 5
#define PB4 4
#define PB3 3
#define PB2 2
#define PB1 1
#define PB0 0
...
Essayez de distinguer la définition d'un registre dans la première partie de la définition d'un bit dans la deuxième partie !
Les possibilités pour atteindre un bit particulier
modifierVoici à travers un exemple comment accéder à un bit particulier pour le registre TWAR. On commence par présenter le fichier d'entête :
....
/* TWAR */
#define TWA6 7
#define TWA5 6
#define TWA4 5
#define TWA3 4
#define TWA2 3
#define TWA1 2
#define TWA0 1
#define TWGCE 0
...
Voici maintenant une utilisation :
void main( void) {
....
//*** Toujours pour set *****
TWAR |=(1<<6);
//**** Si on connaît le nom
TWAR |=(1<<TWA5);
//*** Toujours pour reset *****
TWAR &= ~(1<<6);
//**** Si on connaît le nom
TWAR &= ~(1<<TWA5);
...
}
Exercice 4
modifierSoit le fichier d'entête :
/* TWCR */
#define TWINT 7
#define TWEA 6
#define TWSTA 5
#define TWSTO 4
#define TWWC 3
#define TWEN 2
/* bit 1 reserved (TWI_TST?) */
#define TWIE 0
1°) Dessiner le registre correspondant
2°) En utilisant les masques du TD précédent, écrire les instructions C permettant :
- mise à 1 de TWEA
- mise à 0 de TWWC.
- tester si TWINT est à 1
- tester si TWEN est à 0
3°) Refaire le même travail en utilisant les noms des bits directement.
Remarque : il existe une autre manière de faire ces mises à un et mises à 0 qui utilisent une macro « _BV » :
void main( void) {
....
//*** Toujours pour set *****
//TWAR |= (1<<6) | (1<<5);
//**** Si on connaît le nom
TWAR |= _BV(TWA6) | _BV(TWA5);
//*** Toujours pour reset *****
TWAR &= ~_BV(TAW5);
...
}
Exercice 5
modifierComment peut-on utiliser la macro « _BV » pour mettre à 0 plusieurs bits à la fois ?
Annexe I : Le GNU C
modifierPour compiler manuellement vous lancez :
avr-gcc -g -mmcu=atmega8 -Wall -Os -c hello.c avr-gcc -g -mmcu=atmega8 -o hello.elf -Wl,-Map,hello.map hello.o avr-objcopy -R .eeprom -O ihex hello.out hello.hex
Le format ELF est le format donné par tous les compilateurs du GNU. Cependant les programmateurs utilisent plutôt un format Intel (HEX) d'où la dernière ligne.
Retrouver le code source assembleur
modifierSi vous voulez connaître le code source assembleur :
avr-objdump -S hello.elf
puisqu' hello.elf a été créé avec la deuxième commande à partir de hello.o
Autre manière :
avr-gcc -g -mmcu=atmega8 -Wall -Os -c hello.c -o hello.s
Assembler un programme
modifieravr-as -mmcu=attiny261 --gdwarf2 ../tb1sw.asm avr-ld -o tb1sw.elf a.out
Retrouver les informations
modifieravr-objdump -h -S hello.elf > hello.lss
Retrouver la taille du code
modifierLa commande
avr-objcopy -O binary -R .eeprom hello.elf hello.bin
génère un fichier binaire dont la taille est celle du code.
Fonctions de manipulation de bits
modifierQuelques fonctions non standard de la librairie du C pour AVR sont données :
Remarque : Attention, comme indiqué dans le tableau, certaines fonctions/macros sont obsolètes et leur usage non recommandées.
Fonction | Définition | Exemple |
---|---|---|
_BV(x) | positionne un bit spécifique | char result = _BV(PINA6); // met 64 dans result
|
void sbi (uint8_t port, uint8_t bit) | positionne un bit spécifique à 1 | sbi (PORTB, 3); //#define PORTB 0x18
sbi (PORTB, PINA3);
|
void cbi (uint8_t port, uint8_t bit) | positionne un bit spécifique à 0 | cbi (PORTB, 3); //#define PORTB 0x18
cbi (PORTB, PINA3);
|
uint8_t bit_is_set (uint8_t port, uint8_t bit); | teste si un bit spécifique est à 1 | //#define PINB 0x16
uint8_t result = bit_is_set (PINB, PINB3);
|
uint8_t bit_is_clear (uint8_t port, uint8_t bit); | teste si un bit spécifique est à 0 | //#define PINB 0x16
uint8_t result = bit_is_clear (PINB, PINB3);
|
uint8_t inp (uint8_t port); | lit un registre spécifique et retourne le résultat | //#define SREG 0x3F
uint8_t res = inp (SREG);
|
uint16_t __inw (uint8_t port); | lit un registre spécifique 16 bits et retourne le résultat | //#define TCNT1 0x2C
uint16_t res = __inw (TCNT1);
|
uint16_t __inw_atomic (uint8_t port); | lit un registre spécifique 16 bits et retourne le résultat sans interruption possible | //#define TCNT1 0x2C
uint16_t res = __inw (TCNT1);
|
outp (uint8_t val, uint8_t port); | sort une valeur spécifique sur un port spécifique | //#define PORTB 0x18
outp(0xFF, PORTB);
|
__outw (uint16_t val, uint8_t port); | sort une valeur spécifique 16 bits sur un port spécifique | //#define OCR1A 0x2A
__outw(0xAAAA, OCR1A);
|
__outw_atomic (uint16_t val, uint8_t port); | sort une valeur spécifique 16 bits sur un port spécifique sans interruption | //#define OCR1A 0x0A
__outw_atomic(0xAAAA, OCR1A);
|
Références
modifier