IL n'y a pas de système d'exploitation sur le microcontrôleur qui permette d'utiliser des librairies dynamiques. L'édition de lien ne peut se faire qu'avec des librairies statiques d'extensions .a et d'autres fichiers objets.
Il n'y a pas non plus de téléchargement de programme, mais une programmation du microcontrôleur qui nécessite un outils particulier appelé programmateur. Cet outils dépend de la famille de microcontrôleurs utilisée. Il est, en général, fourni avec la carte application (cible).
Le programmateur peut nécessiter de programmer le circuit en dehors de la carte d'application, on retire le microcontrôleur de la carte d'application pour l'installer dans le programmateur, puis, après programmation, on le replace sur la carte d'application.
Le plus souvent le programmateur permet de programmer le microcontrôleur directement sur la carte d'application, on parle de programmateur ISP (In-System Programming). Cela se fait soit en utilisant des broches dédiées, soit en utilisant un protocole logiciel avec des broches utilisées par l'application.
Les outils de développement peuvent être un ensemble de programmes SDK (Software development kit), ou encore un environnement intégré (IDE pour Integrated development environment), ils sont spécifiques à chaque famille de microcontrôleur.
Les fonctions disponibles pour un microcontrôleur sont également différentes, il n'y a pas de gestion de périphériques de type fichier, ce qui exclut stdio.h, printf, scanf, ...
De plus la fonction main diffère de celle des systèmes avec système d'exploitation, en ce sens qu'il n'y a pas d'arguments de ligne de commande, et qu'un programme de microcontrôleur ne se termine jamais, ce qui justifie la présence d'une boucle infinie qu'il ne faut jamais oublier :
int main(void) {
// initialisation
while (1) {
// fonctionnement permanent
}
return 0;
}
int main(void) {
// initialisation
for(;;) {
// fonctionnement permanent
}
return 0;
}
Certains compilateurs imposent la valeur de retour.
Notez que je décline toutes responsabilités quant aux conséquences que pourraient avoir l'utilisation des programmes présentés. Ceux-ci pourraient être erronés ou obsolètes.
Aux outils décrits dans le chapitre sur la programmation en assembleur, il faut ajouter le cross-compilateur gnu avr gcc ainsi que l'outil de programmation avrdude.
Nous allons utilisé un proche de l'atmega8 utilisé dans le chapitre sur la programmation assembleur, l'atmega328p.
Port B :
Port C :
Port D :
L'objectif étant de programmer le microcontrôleur et aussi de le simuler, on va créer un Makefile qui permet de programmer avec avrdude et aussi de créer le fichier de simulation. Dans le cas où l'exécution du programme nécessite des temps supérieurs au dixième de seconde, on va intégrer la possibilité de paramétrer les temps pour qu'ils soient adaptés à la simulation.
PROCESSEUR=atmega328p
CC=avr-gcc
OBJCOPY=avr-objcopy
CCFLAGS=-mmcu=$(PROCESSEUR) -Wall -MD -MT
LDFLAGS=-mmcu=$(PROCESSEUR) -Wl,--undefined=_mmcu,--section-start=.mmcu=0x910000
OBJCOPYFLAGS=-O ihex -R .eeprom
TEMPSIMULATION=#remplacer par le temps de simulation en ns
SIMULAVR=simulavr
SIMULAVRFLAGS= -d atmega328 -F 16000000 -m $(TEMPSIMULATION)
PERIPHERIQUESERIE=#remplacer par le descripteur série pour le programmateur
PROGRAMMATEUR=#remplacer par le nom du programmateur
AVRDUDE=avrdude
AVRDUDEFLAGS= -p $(PROCESSEUR) -c $(PROGRAMMATEUR) -b 19200 -P $(PERIPHERIQUESERIE)
DIRBUILD=.
SRC=hello.c
OBJ= $(SRC:.c=.o)
ELF= $(SRC:.c=.elf)
HEX= $(DIRBUILD)/$(SRC:.c=.hex)
DFILE= $(SRC:.c=.d)
VCD=$(SRC:.c=.vcd)
TXT=$(SRC:.c=.txt)
all:
$(CC) $(CCFLAGS) $(OBJ) -c $(SRC)
$(CC) $(LDFLAGS) -o $(ELF) $(OBJ)
$(OBJCOPY) $(OBJCOPYFLAGS) $(ELF) $(HEX)
upload:
$(AVRDUDE) $(AVRDUDEFLAGS) -U flash:w:$(HEX)
simule:
$(CC) -DSIMULATION $(CCFLAGS) $(OBJ) -c $(SRC)
$(CC) $(LDFLAGS) -o $(ELF) $(OBJ)
$(SIMULAVR) $(SIMULAVRFLAGS) -f $(ELF) -o $(TXT)
$(SIMULAVR) $(SIMULAVRFLAGS) -f $(ELF) -c vcd:$(TXT):$(VCD)
clean:
rm -f $(DFILE) $(MAP) $(OBJ) $(ELF) $(HEX) $(TXT) $(VCD)
Fichier Makefile générique
La commande make par défaut (all) permet de créer le fichier hex pour le programmateur. Elle comprend la phase de compilation qui crée le fichier objet, la phase d'édition de lien qui crée un fichier au format elf, puis la phase de création du fichier hex pour l'outil de programmation.
La commande make upload programme le circuit cible à partir du fichier hex.
La commande make simule effectue les phases de compilation, édition de lien pour fournir le ficher elf pour l'outil de simulation, qui commence par créer le fichier txt qui contient les différents éléments du processeur pour, ensuite, créer le fichier vcd qui peut être visualiser par gtkwave.
Pour utiliser ce Makefile, il faut changer le nom du fichier source et il faut définir les valeurs des variables suivantes :
Afin de paramétrer les temps dans le code source, le fichier Makefile définit une valeur SIMULATION, qui peut être testée avec une commande de préprocesseur de type #ifdef ou #ifndef.
L'accès aux GPIOs est très proche de la programmation assembleur en utilisant les registres associés aux ports B, C et D. Ces registres sont définis dans avr/io.h et sont :
Exemple avec le classique chenillard
Code soure chenillard.c
#include <avr/io.h>
#define F_CPU 16000000L
#include <util/delay.h>
#ifdef SIMULATION
#define DELAI_MS 1
#else
#define DELAI_MS 500
#endif
int main(void) {
unsigned char octet = 1;
DDRD = 0xff;
while(1) {
PORTD = octet ;
if (octet == 0x80) {
octet = 1 ;
}
else {
octet <<= 1;
}
_delay_ms(DELAI_MS);
}
return 0;
}
Pour utiliser la fonction _delay_ms il faut définir la fréquence du processeur avant d'inclure le fichier util/delay.h.
Avec le Makefile présenté précédemment, #ifdef ... #endif permet de définir le temps de 1ms en simulation et 500ms sur le processeur réel.
Résultat de la simulation
Comme pour les GPIOs, on accède directement aux registres du timer avec les définitions déclarées dans avr/io.h. On utilisera donc la configuration du chapitre sur la programmation en assembleur du timer. Les principaux registres utilisés sont :
On utilise ici la sortie OC1A qui correspond au bit 1 du port B, qu'il faut initialiser en sortie.
Code soure pwm.c
#include <avr/io.h>
#define CONTROLA (( 1 << COM1A1) | ( 1 << COM1A0) | (1 << WGM10))
#define CONTROLB (1 << WGM12)
#ifdef SIMULATION
#define CONTROLBS ((1 << WGM12) | (1 << CS10))
#else
#define CONTROLBS ((1 << WGM12) | (1 << CS12) | (1 << CS10))
#endif
int main(void) {
TCCR1A = CONTROLA;
TCCR1B = CONTROLB;
OCR1AH = 0 ;
OCR1AL = 200;
DDRB = 2;
TCCR1B = CONTROLBS;
while (1) {
}
return 0;
}
On remarque que la boucle infinie ne fait rien, en effet le signal pwm est uniquement matériel. Ceci permet de ne pas occuper le processeur pour le signal pwm et de le laisser libre pour d'autres tâches.
En plus de l'initialisation du timer, on n'oublie pas de définir la broche 1 du port B en sortie.
Calcul de la période et du rapport cyclique
La fréquence de l'horloge est de 16MHz, en mode simulation l'horloge du timer est divisée par 1 et en fonctionnement sur le microcontrôleur l'horloge du timer est divisée par 1024, soit environ 15.8us
Le rapport cyclique est de (255-200)/255 ≈ 0,21. Sur le graphe (visualisé avec gtkwave) on trouve 3,4us pour l'état 1 ce qui donne 3,4/15.8 ≈ 0,21
Résultat de la simulation
Cette fois-ci on utilise le timer pour générer une interruption périodique, qui va déclencher la fonction de gestion du chenillard. Dans ce cas on désactive la broche associée à ce timer. En C la fonction d'interruption possède un nom spécifique, qui est dans ce cas ISR. Le paramètre de cette fonction est l'identifiant du vecteur associé à cette fonction, qui correspond à l'interruption du comparateur A du timer 1 : TIMER1_COMPA_vect.
Afin de mettre en évidence le fonctionnement matériel du timer. On ajoute le clignotement d'une neuvième LED en parallèle avec le chenillard.
Code soure chenillard.c
#include <avr/io.h>
#include <avr/interrupt.h>
#define F_CPU 16000000L
#include <util/delay.h>
#define CONTROLA 0
#define CONTROLB (1 << WGM12)
#ifdef SIMULATION
#define DELAI_MS 1
#define CONTROLBS ((1 << WGM12) | (1 << CS10))
#else
#define DELAI_MS 500
#define CONTROLBS ((1 << WGM12) | (1 << CS12) | (1 << CS10))
#endif
#define DIVISEUR 7812
#define DIVMSB ((DIVISEUR >> 8)& 0xff)
#define DIVLSB (DIVISEUR & 0xff)
int main(void) {
TIMSK1 = 0;
TCCR1A = CONTROLA;
TCCR1B = CONTROLB;
TCCR1C = 0 ;
OCR1AH = DIVMSB ;
OCR1AL = DIVLSB;
DDRD = 0xff;
PORTD = 1 ;
DDRB = 2;
PORTB = 2 ;
TIMSK1 = ( 1 << OCIE1A);
sei();
TCCR1B = CONTROLBS;
while (1) {
_delay_ms(DELAI_MS);
PORTB ^= 2;
}
return 0;
}
ISR(TIMER1_COMPA_vect) {
PORTD <<= 1;
if (PORTD == 0) {
PORTD=1;
}
}
Le calcul de la période du chenillard est défini par la fréquence de l'horloge 16Mz, la configuration du diviseur du timer (1024 en réel, 1 en simulation) ainsi que de la valeur des registres OCR1AH et OCR1AL. On a donc une horloge de 16MHz divisée par 7812 qui donne ≈ 2048Hz, cela donne une période d'environ 488us en simulation et 0,5s en réalité.
La demi-période de clignotement de la neuvième LED est de 1ms en simulation et 0.5s en réalité.
Le chenillard est donc décalé par rapport au clignotement de la LED.
Afin de répartir la valeur du diviseur sur les registres de poids fort et poids faible, on fait effectuer le calcul des octets de poids forts et faibles par le préprocesseur à l'aide de directives define.
La fonction sei autorise la prise en compte d'interruptions par le processeur.
Résultat de la simulation
On propose de programmer le microcontrôleur en utilisant l'interface SPI de la raspberry avec avrdude. L'option linuxspi n'est pas forcement disponible avec la version fournie. Si c'est le cas, il faut désinstaller la version fournie, et en installer une autre version disponible sur github.
Les broches de la raspberry pi fonctionnent en 3.3v, il faut donc connecter l'alimentation du processeur sur la broche 3.3v du connecteur de la raspberry pi.
La vidéo présente la maquette raspberry pi avec le programme de chenillard avec timer en interruption sur le processeur atmega328p. Les broches SPI de l'atmega328p sont connectées aux broches SPI de la raspberry pi. Ce processeur est configuré pour fonctionner avec l'horloge interne de 8MHz sans quartz. On aperçoit le processeur sous les fils en bas à gauche de la vidéo. Le port D est connecté aux 8 leds et la broche PB1 est connectée à la neuvième led qui clignote indépendamment des 8 autres leds.
La carte Arduino uno est utilisée pour le chapitre suivant.
PROCESSEUR=atmega328p
CC=avr-gcc
OBJCOPY=avr-objcopy
CCFLAGS=-mmcu=$(PROCESSEUR) -Wall -MD -MT
LDFLAGS=-mmcu=$(PROCESSEUR) -Wl,--undefined=_mmcu,--section-start=.mmcu=0x910000
OBJCOPYFLAGS=-O ihex -R .eeprom
TEMPSIMULATION=#remplacer par le temps de simulation en ns
SIMULAVR=simulavr
SIMULAVRFLAGS= -d atmega328 -F 16000000 -m $(TEMPSIMULATION)
PERIPHERIQUESERIE=/dev/spidev0.0:/dev/gpiochip0:21
PROGRAMMATEUR=linuxspi
AVRDUDE=avrdude
AVRDUDEFLAGS= -p $(PROCESSEUR) -c $(PROGRAMMATEUR) -P $(PERIPHERIQUESERIE)
DIRBUILD=.
SRC=hello.c
OBJ= $(SRC:.c=.o)
ELF= $(SRC:.c=.elf)
HEX= $(DIRBUILD)/$(SRC:.c=.hex)
DFILE= $(SRC:.c=.d)
VCD=$(SRC:.c=.vcd)
TXT=$(SRC:.c=.txt)
all:
$(CC) $(CCFLAGS) $(OBJ) -c $(SRC)
$(CC) $(LDFLAGS) -o $(ELF) $(OBJ)
$(OBJCOPY) $(OBJCOPYFLAGS) $(ELF) $(HEX)
upload:
$(AVRDUDE) $(AVRDUDEFLAGS) -U flash:w:$(HEX)
gpioset giochip0 21=1
simule:
$(CC) -DSIMULATION $(CCFLAGS) $(OBJ) -c $(SRC)
$(CC) $(LDFLAGS) -o $(ELF) $(OBJ)
$(SIMULAVR) $(SIMULAVRFLAGS) -f $(ELF) -o $(TXT)
$(SIMULAVR) $(SIMULAVRFLAGS) -f $(ELF) -c vcd:$(TXT):$(VCD)
clean:
rm -f $(DFILE) $(MAP) $(OBJ) $(ELF) $(HEX) $(TXT) $(VCD)
Le programmateur est luinuxspi, et le périphérique est /dev/spidev0.0:/dev/gpiochip0:21 pour l'interface gpiochip0 et le descripteur /dev/spidev0:0, 21 correspond à la broche gpio qui est connectée au reset du microcontrôleur à programmer.
Après avoir programmé le circuit, avrdude ne remet pas la broche reset à 1, ce qui laisse le microcontrôleur en état de reset. Il faut utiliser la commande gpioset gpiochip0 21=1 qui repositionne la broche reset à 1 et démarre le programme du microcontrôleur.