Utiliser un Arduino sans code Arduino : ATmega328P
Vous avez probablement déjà entendu parler d’Arduino, au moins en tant que carte électronique programmable pour bidouilleur (d’ailleurs j’en ai utilisé dans mes précédents articles).
Si vous connaissez déjà ces cartes c’est bien, si vous avez déjà testé un petit programme Arduino c’est mieux !!
En effet, cet article n’a pas pour objectif de vous présenter ni de vous initier à l’Arduino, mais plutôt d’explorer différentes façons d’écrire du code et voir quels sont les avantages et inconvénients de l’environnement Arduino, et surtout comment créer des programmes Arduino sans code Arduino.
Introduction
Arduino ce n’est pas seulement des cartes électroniques programmables, c’est aussi un IDE et surtout un framework, le tout open-source !
Ce framework est sujet à débat … certains lui reprochent de trop simplifier la programmation des µC
(microcontrôleurs), ce qui rend le programme plus lourd (en terme de mémoire et d’exécution), pas optimisé et “limite certaines possibilités”.
Enfin, ce sont les principaux arguments qu’on retrouve contre ce framework, mais nous allons voir que malgré ces points négatifs, il y a aussi du positif et ce n’est pas négligeable !
Concernant l’IDE Arduino, tout le monde semble plus ou moins d’accord sur le fait que cet environnement de développement est totalement dépassé !
C’est un véritable calvaire de coder dans cet IDE, même si c’est toujours mieux que le vieux notepad natif de Windows … (ouai bon, j’exagère un peu, c’est pas bien compliqué de faire mieux que ce vieux notepad ^^).
A l’heure actuelle, ils travaillent sur une nouvelle version de l’IDE Arduino (version 2.0 dispo en beta). Même si elle semble bien mieux que la première version, il semblerait qu’elle ne soit pas encore prête à remplacer un véritable IDE.
C’est quoi un microcontrôleur ?
Si vous n’êtes pas déjà familier avec les cartes électroniques programmables, il est possible que vous ne saisissiez pas bien ce qu’est un µC
(microcontrôleurs).
Sans rentrer dans les détails, un µC
est une puce électronique qui contient tous les éléments (ou presque) nécessaires à l’exécution d’un code et à la manipulation d’entrées/sorties et/ou l’utilisation de certains périphériques (DAC/ADC, liaison série, timer etc…).
Chaque modèle de carte électronique programmable est basé sur un modèle de µC
(dans certains cas, suivant la version d’une carte on peut trouver un µC légèrement différent).
Par exemple, le très célèbre Arduino UNO utilise le non moins célèbre µC
ATmega328 (tout comme l’Arduino Nano, mais ce dernier existe aussi avec un ATmega168).
Ces cartes contiennent généralement assez peu de composants.
Une grande partie des µC
ont uniquement besoin d’une alimentation pour fonctionner. D’autres vont également avoir besoin d’un oscillateur externe, voir même d’une mémoire externe (ou plusieurs), puis suivant les périphériques présents dans le µC
on peut avoir besoin d’autres composants comme une antenne par exemple …
Le rôle principal de ce genre de carte est donc de faciliter les connexions, ce sont des cartes de développement qui permettent de réaliser des tests facilement et rapidement, notamment en intégrant (généralement) une interface USB pour programmer facilement le µC
(la partie USB est gérée soit par une puce dédiée sur la carte, soit par le µC
directement si il est compatible avec cette fonctionnalité).
Mais on pourrait très bien utiliser le µC
en dehors de sa carte en lui apportant le minimum de composants requis pour qu’il fonctionne (donc pas grand chose).
Concept de l’IDE Arduino
Malgré ses défauts, cet environnement de développement Arduino a l’avantage de simplifier énormément la compilation et le transfert des programmes pour toutes les cartes compatibles Arduino grâce à un gestionnaire de librairies très facile à utiliser ainsi qu’un gestionnaire de cartes qui permet d’ajouter de nouvelles cartes et/ou de nouvelles options pour certaines cartes.
En quelques cliques on peut donc compiler et téléverser (hmm, alors, cette traduction est aussi sujet à débat, mais c’est la traduction officielle …) le programme sur une carte gérée par l’IDE.
Heureusement, vous pouvez éditer votre code dans votre éditeur favori puis utiliser l’IDE Arduino uniquement pour la compilation et la programmation de votre carte.
Il existe évidemment d’autres alternatives pour ne pas utiliser l’IDE Arduino, comme PlatformIO, mais je ne vais pas traiter ce sujet ici.
Dans cet article nous verrons comment nous passer du framework et de l’IDE Arduino en utilisant directement les outils AVR (voir dans les prochains chapitres) !
Concept du framework Arduino
Le framework Arduino a pour objectif de rendre facilement accessible la programmation d’un certain nombre de µC
, mais aussi de rendre compatible le code pour une grande partie des µC
compatibles Arduino.
En effet, chaque modèle de µC
a ses propres caractéristiques/spécificités, notamment au niveau de la répartition des GPIO et des adresses mémoire à utiliser pour atteindre ces GPIO ainsi que les différents périphériques du µC
.
En utilisant le framework, inutile d’aller lire la documentation du µC
pour savoir quelle adresse mémoire (ou registre) utiliser etc…, c’est le framework qui gère tout ça pour nous !
Cela permet donc d’avoir un code identique pour une grande partie des µC
compatibles Arduino.
Ce qui est un sacré avantage pour l’apprentissage et pour le partage !
Evidemment, si vous partagez un code qui utilise 50 GPIO d’un Arduino Mega … vous ne pourrez pas l’utiliser directement sur un Arduino UNO (qui possède seulement 20 GPIO).
Le framework n’est pas magique, il ne va pas créer ce qui n’existe pas … (pour certaines fonctionnalités ou certains périphériques il peut proposer une solution software quand c’est possible).
Comparaison d’un code Arduino avec et sans framework
J’ai hésité à faire des benchmark pour comparer les performances d’un “même” code avec et sans framework mais ça impliquerait de rentrer beaucoup plus dans les détails de tout un tas de fonctionnalités et ça ferait un article interminable … (peut-être pour un prochain article)
Donc pour simplifier la comparaison, je me suis limité à comparer la taille des binaires (la quantité de mémoire flash utilisée pour stocker le programme dans le µC
).
Quand on parle de programme de test sur un Arduino, le plus populaire est sans conteste le Blink
qui permet tout simplement de faire clignoter une LED à intervalle régulier.
Généralement, les cartes Arduino ont une LED intégrée sur la carte, ce qui permet de tester directement sans aucun branchement autre que l’alimentation.
On va voir ci-dessous différentes façons de réaliser ce Blink
.
J’utilise un Arduino UNO pour ce programme de test ! C’est donc un µC
ATmega328 (processeur AVR, donc utilisation de la lib et des outils AVR).
1 - Code Arduino, Compilateur IDE Arduino
Le code Blink
officiel :
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
Mémoire utilisée : 924 octets
Compatibilité : Toutes les cartes compatibles Arduino
Certaines cartes n’ont pas de LED intégrée, auquel cas il faut l’ajouter pour le test.
2 - Code Arduino optimisé 1, Compilateur IDE Arduino
Première optimisation en remplaçant delay
du framework Arduino par _delay_ms
de la lib AVR :
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
digitalWrite(LED_BUILTIN, HIGH);
_delay_ms(1000);
digitalWrite(LED_BUILTIN, LOW);
_delay_ms(1000);
}
Mémoire utilisée : 770 octets
Compatibilité : Cartes Arduino basées sur un µC AVR
Et éventuellement d’autres si la fonction _delay_ms
existe dans la lib du µC en question.
154 octets
de gagnés juste en utilisant une autre fonction de délais, mais au détriment d’une compatibilité limitée aux µC AVR.
3 - Code Arduino optimisé 2, Compilateur IDE Arduino
Seconde optimisation en utilisant directement les registres pour configurer et manipuler le GPIO LED_BUILTIN
(qui correspond au GPIO 13) :
void setup(){
DDRB = 0b00100000;
}
void loop(){
PORTB |= 0b00100000;
_delay_ms(1000);
PORTB &= 0b11011111;
_delay_ms(1000);
}
Mémoire utilisée : 488 octets
Compatibilité : Cartes Arduino basées sur un µC AVR et dont le bit 5 du registre B correspond bien au bon GPIO
Parmi les cartes Arduino officielles les plus connues, les suivantes son compatibles : Duemilanove, Mini, Nano, Pro Mini, Uno
Pour déterminer le registre et le bit à utiliser, soit il faut lire le datasheet du µC
, soit il faut regarder le pinout de la carte
et/ou celui du µC
(pinout arduino uno dans une recherche d’images).
Dans le cas présent, on constate que le GPIO 13 est sur le port PB5
, ce qui correspond au port/registre B
et au bit 5
.
L’utilisation des registres permet, dans le cas présent, d’économiser 282 octets
!
Cependant, les registres dépendent des µC … comme évoqué précédemment, d’un modèle de µC à un autre, les GPIO et autres périphériques sont généralement attribués à des adresses/registres/ports différents.
Mais il y a au moins deux gros avantages à utiliser les registres :
- Au niveau des performances c’est le top, on ne pourra pas faire mieux (et comparé aux fonctions du framework c’est un gain énorme en performance)
- On peut manipuler plusieurs GPIO d’un coup ! Dans le cas présent on peut manipuler 8 GPIO d’un coup (sur un ESP32 on peut manipuler 32 GPIO d’un coup)
4 - Code avec lib AVR uniquement, Compilateur IDE Arduino
__attribute__((weak))
int main(){
DDRB = 0b00100000;
for(;;){
PORTB |= 0b00100000;
_delay_ms(1000);
PORTB &= 0b11011111;
_delay_ms(1000);
}
return 0;
}
Mémoire utilisée : 178 octets
Compatibilité : Même compatibilité que le code précédent
Les fonctions setup
et loop
font partie du framework Arduino, mais derrière ces deux fonctions il y a un main
!
En réécrivant la fonction main
on gagne 310 octets
!
Cet écart est principalement lié à l’appel de la fonction init
qui est exécuté au début du main
du framework.
Cette fonction init
permet d’activer les interruptions et de paramétrer les registres des timers et de l’ADC.
Pour notre Blink
on peut s’en passer, mais si le programme a besoin d’utiliser certaines de ces fonctionnalités alors il faudra s’assurer qu’ils sont bien paramétrés.
5 - Code avec lib AVR uniquement, Compilateur avr-gcc
#define F_CPU 16000000UL
#include <avr/io.h>
#include <util/delay.h>
int main(){
DDRB = 0b00100000;
for(;;){
PORTB |= 0b00100000;
_delay_ms(1000);
PORTB &= 0b11011111;
_delay_ms(1000);
}
return 0;
}
Mémoire utilisée : 178 octets
Compatibilité : Même compatibilité que le code précédent MAIS il faut indiquer la fréquence de fonctionnement du µC !
Dans le cas présent, le fait de définir une mauvaise fréquence ne devrait pas bloquer le programme, mais la fonction _delay_ms
ne sera pas cohérente.
Exactement le même poids qu’avec le code précédent, et c’est parfaitement normal puisqu’il s’agit du même code mais avec les instructions nécessaires à la compilation en dehors de l’IDE Arduino.
6 - Code sans framework et sans lib, Compilateur avr-gcc
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
typedef unsigned long int uint32_t;
#define F_CPU 16000000UL
#define DDRB (*(volatile uint8_t *)(0x24))
#define PORTB (*(volatile uint8_t *)(0x25))
void _delay_loop_2(uint16_t __count){
__asm__ volatile (
"1: sbiw %0,1" "\n\t"
"brne 1b" : "=w" (__count) : "0" (__count)
);
}
void _delay_ms(double __ms){
double __tmp;
uint16_t __ticks;
__tmp = ((F_CPU) / 4e3) * __ms;
if (__tmp < 1.0)
__ticks = 1;
else if (__tmp > 65535)
{
__ticks = (uint16_t) (__ms * 10.0);
while(__ticks)
{
_delay_loop_2(((F_CPU) / 4e3) / 10);
__ticks--;
}
return;
}
else
__ticks = (uint16_t)__tmp;
_delay_loop_2(__ticks);
}
int main(){
DDRB = 0b00100000;
for(;;){
PORTB |= 0b00100000;
_delay_ms(1000);
PORTB &= 0b11011111;
_delay_ms(1000);
}
return 0;
}
Mémoire utilisée : 174 octets
Compatibilité : Même compatibilité que le code précédent
J’ai simplement copié une partie de la fonction de la lib AVR pour gérer le délais. Il y a aucun intérêt à faire ça mais c’était pour montrer cette possibilité.
Vous en conviendrez, le gain de 4 octets
ne justifie clairement pas de faire ce genre de code.
Récapitulatif
Lib | Compilateur | Mémoire (octets) | Compatibilité | |
---|---|---|---|---|
Code 1 |
Arduino | IDE Arduino | 924 | Arduino |
Code 2 |
Arduino + AVR | IDE Arduino | 770 | µC AVR |
Code 3 |
Arduino + AVR | IDE Arduino | 488 | µC AVR avec les mêmes registres |
Code 4 |
AVR | IDE Arduino | 178 | µC AVR avec les mêmes registres |
Code 5 |
AVR | avr-gcc | 178 | µC AVR avec les mêmes registres |
Code 6 |
- | avr-gcc | 174 | µC AVR avec les mêmes registres |
Dès qu’on utilise des éléments de la lib AVR on se retrouve limité aux µC AVR
, et dès qu’on utilise des éléments spécifiques à un µC
(comme les registres) on restreint encore plus la compatibilité du code.
L’IDE Arduino utilise également avr-gcc pour les cartes basées sur un µC AVR
, mais il va automatiquement inclure son framework ainsi qu’une grande partie de la lib AVR.
C’est pour cela que les codes compilés dans l’IDE Arduino n’ont pas besoin d’include pour utiliser les fonctionnalités du framework et une grande partie des fonctionnalités de la lib AVR.
Pour compiler avec avr-gcc j’ai utilisé exactement les mêmes options que l’IDE Arduino (sans forcément chercher à quoi elles servent), le but n’étant pas de voir si les options de compilation sont réellement optimales mais plutôt de comparer le résultat final avec les même options compilation.
Voici les commandes pour compiler et récupérer le binaire :
avr-gcc -Wall -Wextra -Os -g -flto -fuse-linker-plugin -Wl,--gc-sections -mmcu=atmega328p blink.c -o blink.elf
avr-objcopy -O binary blink.elf blink.bin
Et la commande pour programmer votre Arduino :
avrdude -c arduino -p m328p -P COMX -b 115200 -U flash:w:blink.bin
Evidemment il faut remplacer COMX
(/dev/ttySX
sous linux) par le bon port série.
Si votre Arduino possède un ancien bootloader, vous devrez passer en baudrate 57600 (-b 57600
) au lieu de 115200.
Au besoin, adaptez également l’option -p
pour correspondre à votre µC
(pour obtenir la liste, tapez cette commande : avrdude -c avrisp
).
Conclusion
La différence de poids peut sembler énorme, et elle l’est pour un simple Blink
!
Mais sur un programme bien plus gros, la différence de poids serait beaucoup moins impressionnante.
Certes on gagnera toujours de l’espace en limitant l’utilisation des fonctions Arduino, mais toujours au détriment de la compatibilité entre les différents µC
.
Et comme évoqué précédemment, il y a aussi les performances qui rentrent en jeu !
L’utilisation des registres permet vraiment un gros gain, autant en poids qu’en performance.
Suivant les contraintes de votre projet vous n’aurez peut-être pas le choix, si vous avez besoin d’optimiser au maximum les performances, et éventuellement la mémoire nécessaire, alors il faudra abandonner le framework Arduino et donc abandonner la compatibilité Arduino.
Quoi qu’il en soit, pour débuter avec les µC
, le framework Arduino est vraiment très bien conçu !
Pour partager son projet avec un maximum de personnes et surtout permettre de réutiliser votre code facilement, il est également très intéressant d’utiliser ce framework.
C’est donc des choix à faire … ça va dépendre de votre projet, de votre cible et de vos préférences.
Evidemment, il existe aussi des µC
qui ne sont pas compatibles Arduino, il y a un choix énorme dans ce domaine.
Si c’est pour une utilisation pro, je pense que vous ne devriez même pas vous demander si il faut utiliser ou non le framework Arduino …
Pour moi l’utilisation d’Arduino est essentiellement pour l’apprentissage, le partage et le bidouillage !
Donc pour terminer, j’estime qu’il est inacceptable de cracher sur le framework Arduino (malgré ses points négatifs), et je pense que ceux qui le font n’ont pas la bonne approche.
Grâce à Arduino, l’électronique et la programmation embarquée se démocratisent de plus en plus et sont désormais très abordables pour tout le monde !