I. Introduction à TinyGo▲
TinyGo est conçu principalement pour les microcontrôleurs 32 bits, les cartes à base d’AVR ont des limitations et ne sont pas capables d’utiliser toutes les capacités de TinyGo, et de plus, ne peuvent faire tourner que de petits programmes.
D’un autre côté, les Arduino Uno et Nano sont très populaires chez les makers, et leurs clones sont à un prix encore plus abordable. Si vous grillez une carte accidentellement, vous ne perdez pas grand-chose. Et dans certaines régions du globe, les Uno ou ses clones sont beaucoup plus accessibles que les Cortex M0/M4 et autres cartes nRF52.
Le langage Go est aussi devenu populaire ces dernières années. Apprendre les bases de ce langage sur un dispositif interagissant avec le monde physique peut être tout aussi intéressant que de l’apprendre uniquement sur un ordinateur personnel. Il est aussi excitant de voir que l’on peut faire tourner des programmes avec un langage autre que le puissant, mais aussi intimidant C++. (Entre autres, plus de problèmes causés par des points-virgules oubliés !)
L’Arduino Uno n’est sans doute pas le support idéal pour TinyGo, mais on peut quand même faire beaucoup de choses avec.
II. Installation et configuration de Go et TinyGo sur Linux Debian/Ubuntu▲
Avant d’installer TinyGo, il vous faut commencer par l’installation du package du langage Go sur votre système.
Seule l’installation de Go et TinyGo sur une distribution Linux Debian/Ubuntu sera décrite dans ce tutoriel. Pour plus de détails, ou pour une installation sur un autre système (Windows, Linux et macOS), référez-vous à la documentation sur golang.org et tinygo.org.
Auparavant, assurez-vous d’avoir les dernières mises à jour de votre système :
$
sudo apt-get update
$
sudo apt-get -y upgrade
II-A. Installation et configuration de Go▲
L’archive .tar.gz de la dernière version stable du langage Go peut être téléchargée en suivant ce lien : https://golang.org/dl/
La dernière version à ce jour est la 1.13.8, et vous pouvez aussi récupérer l’archive avec la commande :
$
wget https://dl.google.com/go/go1.13
.8
.linux-amd64.tar.gz
Dans ce tutoriel, on choisit d’installer le package Go dans le répertoire usr/local. Pour cela, tapez les commandes :
$
sudo tar -xvf go1.13
.8
.linux-amd64.tar.gz
$
sudo mv go /usr/local
Vous devez maintenant configurer les trois variables d’environnement du langage Go : GOROOT, GOPATH et PATH.
GOROOT définit l’emplacement où est installé le package Go sur votre système :
$
export GOROOT
=
/usr/local
/go
GOPATH définit l’emplacement de votre répertoire de travail. Par exemple, ici, ~/MesProjets/ :
$
export GOPATH
=
$HOME
/MesProjets
Enfin, le PATH est complété avec l’emplacement des fichiers binaires :
$
export PATH
=
$GOPATH
/bin:$GOROOT
/bin:$PATH
Ici, les variables d’environnement sont uniquement configurées pour la session en cours. Si vous voulez rendre la configuration permanente, il vous suffit de copier-coller les trois commandes précédentes à la fin du fichier ~/.profile.
Rendu ici, vous avez installé et configuré le langage Go avec succès sur votre système.
Commencez par utiliser la commande suivante pour voir la version installée :
$
go version
go version go1.13
.8
linux/amd64
Et pour vérifier les variables d’environnement :
$
go env
GO111MODULE
=
""
GOARCH
=
"amd64"
GOBIN
=
""
GOCACHE
=
"/home/dvp/.cache/go-build"
GOENV
=
"/home/dvp/.config/go/env"
GOEXE
=
""
GOFLAGS
=
""
GOHOSTARCH
=
"amd64"
GOHOSTOS
=
"linux"
GONOPROXY
=
""
GONOSUMDB
=
""
GOOS
=
"linux"
GOPATH
=
"/home/dvp/MesProjets"
GOPRIVATE
=
""
GOPROXY
=
"https://proxy.golang.org,direct"
GOROOT
=
"/usr/local/go"
GOSUMDB
=
"sum.golang.org"
GOTMPDIR
=
""
GOTOOLDIR
=
"/usr/local/go/pkg/tool/linux_amd64"
GCCGO
=
"gccgo"
AR
=
"ar"
CC
=
"gcc"
CXX
=
"g++"
...
II-B. Installation et configuration de TinyGo▲
Commencez par récupérer le fichier .deb du package TinyGo depuis Github et lancez l’installation avec les commandes suivantes :
$
wget https://github.com/tinygo-org/tinygo/releases/download/v0.12
.0
/tinygo_0.12
.0_amd64.deb
$
sudo dpkg -i tinygo_0.12
.0_amd64.deb
Ajoutez le chemin vers l’exécutable de TinyGo dans votre variable d’environnement PATH :
$
export PATH
=
$PATH
:/usr/local
/tinygo/bin
Ajoutez cette dernière ligne dans votre fichier ~/.profile pour rendre la configuration permanente.
Il vous reste à tester si l’installation s’est parfaitement déroulée en exécutant la commande qui devrait afficher le numéro de version de TinyGo :
$
tinygo version
tinygo version 0
.12
.0
linux/amd64 (
using go version go1.13
.8
)
Pour compiler et téléverser des programmes TinyGo dans un microcontrôleur AVR comme celui de l’Arduino Uno, vous devez encore installer des outils supplémentaires :
$
sudo apt-get install gcc-avr
$
sudo apt-get install avr-libc
$
sudo apt-get install avrdude
Cette fois, vous êtes prêts pour une première démonstration…
III. Un premier programme, l’exemple Blinky▲
Une broche GPIO (General Purpose Input/Output) peut servir à piloter un composant extérieur, une LED par exemple. La broche peut être active avec sa sortie au niveau logique haut (5 V), ou non active avec sa sortie au niveau logique bas (GND), ce qui aura pour effet d’allumer ou éteindre la LED.
Ci-dessous, le code classique Blink pour faire clignoter la LED intégrée en surface de la carte Arduino et reliée à la broche 13 (code légèrement modifié par rapport à l’exemple officiel) :
package
main
import
(
"
machine
"
"
time
"
)
func
main()
{
// Utilisation de la LED intégrée en surface de la carte, broche D13
var
led machine.
Pin =
machine.
Pin(
13
)
// Configuration de la broche en sortie
led.
Configure(
machine.
PinConfig{
Mode:
machine.
PinOutput})
for
{
led.
High()
// sortie au niveau logique haut, LED allumée
time.
Sleep(
time.
Millisecond *
500
)
// temporisation 500 millisecondes
led.
Low()
// sortie au niveau logique bas, LED éteinte
time.
Sleep(
time.
Millisecond *
500
)
// temporisation 500 millisecondes
}
}
Le fichier blinky.go est par exemple sauvegardé dans le dossier ~/MesProjets/src/blinky.
Préparez votre carte Arduino en la connectant sur le port USB. Un nouveau port devrait apparaître avec la commande :
$
ls -l /dev/tty*
En général, il s’agit du port /dev/ttyACM0.
Avant de téléverser le programme dans la carte, il vous faudra probablement modifier les droits d’accès au port USB (message Error opening serial port…). La raison est que le propriétaire du fichier /dev/ttyACM0 fait partie du groupe dialout :
crw-rw---- 1
root dialout 166
, 0
févr. 26
10
:49
/dev/ttyACM0
Il faut donc ajouter l’utilisateur à ce groupe :
$
sudo usermod -a -G dialout <
username>
où <username> est le nom utilisateur. Il faudra vous déconnecter et vous reconnecter pour que cela prenne effet.
Plus de détails sur le guide Linux Arduino.
Pour compiler et téléverser le programme, exécutez la commande :
$
tinygo flash -target arduino -port /dev/ttyACM0 blinky
Ici, la cible est arduino pour l’Arduino Uno. Pour l’Arduino Nano, utilisez la cible arduino-nano.
avrdude: AVR device initialized and ready to accept instructions
Reading | ################################################## | 100% 0.01s
avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "/tmp/tinygo110300084/main.hex"
avrdude: writing flash (552 bytes):
Writing | ################################################## | 100% 0.14s
avrdude: 552 bytes of flash written
avrdude: verifying flash memory against /tmp/tinygo110300084/main.hex:
avrdude: load data flash data from input file /tmp/tinygo110300084/main.hex:
avrdude: input file /tmp/tinygo110300084/main.hex contains 552 bytes
avrdude: reading on-chip flash data:
Reading | ################################################## | 100% 0.10s
avrdude: verifying ...
avrdude: 552 bytes of flash verified
avrdude done. Thank you.
Et voici le résultat…
IV. L’exemple Blinky : version alternative▲
Cette fois, une LED externe sera connectée à la broche 13. Notez la présence de la résistance de 220 Ω pour limiter le courant et protéger la LED.
package
main
import
(
"
machine
"
"
time
"
)
func
main()
{
led :=
machine.
Pin(
13
)
// équivalent à var led = machine.LED
led.
Configure(
machine.
PinConfig{
1
})
// 1 = output mode, 0 = input mode
led_switch :=
true
// led switch
for
{
led.
Set(
led_switch)
led_switch =
!
led_switch // inversion de l’état
delay(
500
)
}
}
func
delay(
t int64
)
{
// fonction de temporisation
time.
Sleep(
time.
Duration(
1000000
*
t))
}
machine.LED est prédéfini dans le module machine, et est équivalent à machine.Pin(13).
On voit aussi qu’on peut remplacer Mode:
machine.
PinOutput par la valeur 1. La méthode .
Configure()
accepte un paramètre PinConfig qui est une structure struct
avec un seul champ Mode :
type
PinConfig struct
{
Mode PinMode
}
Mode est en fait un entier uint8
:
type
PinMode uint8
const
(
PinInput PinMode =
iota
// 0
PinOutput // 1
)
Cependant, vous pourriez avoir besoin de passer par cette constante pour faciliter la lecture du code.
Vous pouvez déclarer une variable sans spécifier son type si vous lui donnez une valeur à la déclaration. Ici, la variable led prendra automatiquement le type machine.Pin.
:=
(opérateur de déclaration de variable court) permet comme var
de déclarer et initialiser des variables, mais ne fonctionne qu’à l’intérieur d’une fonction (fonction main()
, par exemple).
Une fonction utilisateur delay()
a été écrite, afin de rendre les choses un peu plus simples.
La plus petite unité de temps donnée par time.
Duration(
int16
)
est la nanoseconde, et en multipliant la durée par 1 000 000 on obtient la milliseconde. (Au cas où vous vous poseriez la question, time.
Since()
ne fonctionne pas avec l’Arduino Uno. J’ai essayé…)
V. Un tableau de LED, façon K2000 (version simplifiée)▲
On crée maintenant un alignement de 9 LED qui s’allument et s’éteignent chacune leur tour, de façon cyclique. Les broches 2 à 10 seront utilisées pour cela.
package
main
import
(
"
machine
"
"
time
"
)
const
max_led_num =
9
// nombre de LED
func
main()
{
var
leds =
[
max_led_num]
machine.
Pin{
machine.
Pin(
2
),
machine.
Pin(
3
),
machine.
Pin(
4
),
machine.
Pin(
5
),
machine.
Pin(
6
),
machine.
Pin(
7
),
machine.
Pin(
8
),
machine.
Pin(
9
),
machine.
Pin(
10
),
}
// tableau des broches
for
i :=
0
;
i <
len
(
leds);
i++
{
// configuration des sorties
leds[
i].
Configure(
machine.
PinConfig{
1
})
}
for
{
// Allumer-éteindre LED de la broche 2 à 10
for
i :=
0
;
i <
len
(
leds);
i++
{
leds[
i].
High()
delay(
75
)
leds[
i].
Low()
}
// Allumer-éteindre LED de la broche 10 à 2
for
i :=
len
(
leds)
-
1
;
i >=
0
;
i--
{
leds[
i].
High()
delay(
75
)
leds[
i].
Low()
}
}
}
func
delay(
t int64
)
{
time.
Sleep(
time.
Duration(
1000000
*
t))
}
VI. Un tableau de LED, façon K2000 (version améliorée)▲
package
main
import
(
"
machine
"
"
time
"
)
const
(
max_led_num =
9
start_led_pin =
2
)
func
main()
{
delay :=
func
(
t int64
)
{
// fonction de temporisation implicite
time.
Sleep(
time.
Duration(
1000000
*
t))
}
var
leds [
max_led_num]
machine.
Pin // tableau de LED
index,
delta :=
0
,
1
// indice position et sens de parcours
for
i :=
0
;
i <
len
(
leds);
i++
{
// configuration des sorties
leds[
i]
=
machine.
Pin(
i +
start_led_pin)
leds[
i].
Configure(
machine.
PinConfig{
1
})
}
for
{
// allumer la LED à la position donnée par l’indice et éteindre les autres
for
i,
led :=
range
leds {
led.
Set(
index ==
i)
}
index +=
delta // position suivante
// inversion du sens de parcours en fin de parcours
if
index ==
0
||
index ==
len
(
leds)
-
1
{
delta *=
-
1
}
delay(
75
)
}
}
delay est une fonction implicite déclarée à l’intérieur de la fonction main()
.
La méthode Set()
pour une led accepte les booléens (true
/false
). Mais en Go, vous ne pouvez par convertir des entiers en booléens, il faut passer par les opérateurs logiques.
En fait, vous n’avez pas du tout besoin de stocker les numéros de broches dans des variables ou de passer par un tableau :
package
main
import
(
"
machine
"
"
time
"
)
const
(
max_led_num =
9
start_led_pin =
2
)
func
main()
{
index,
delta :=
0
,
1
for
i :=
0
;
i <
max_led_num ;
i++
{
machine.
Pin(
i +
start_led_pin).
Configure(
machine.
PinConfig{
1
})
}
for
{
for
i :=
0
;
i <
max_led_num ;
i++
{
machine.
Pin(
i +
start_led_pin).
Set(
index ==
i)
}
index +=
delta
if
index ==
0
||
index ==
max_led_num -
1
{
delta *=
-
1
}
delay(
75
)
}
}
func
delay(
t int64
)
{
time.
Sleep(
time.
Duration(
1000000
*
t))
}
VII. Boutons-poussoirs : entrée numérique▲
Les broches peuvent aussi fonctionner en entrée afin de lire l’état haut ou bas du signal numérique. Ici nous utiliserons un bouton-poussoir (ou un interrupteur) connecté sur la broche 8 afin d’allumer ou éteindre la LED reliée à la broche 13.
Pour définir un état haut ou bas, il faut câbler le bouton avec une résistance de tirage pull-up de 10 kΩ.
package
main
import
(
"
machine
"
"
time
"
)
func
main()
{
// configurer la broche 8 en entrée pour lire l’état du bouton
button :=
machine.
Pin(
8
)
button.
Configure(
machine.
PinConfig{
0
})
// identique à machine.PinConfig{Mode: machine.PinInput}
led :=
machine.
Pin(
13
)
led.
Configure(
machine.
PinConfig{
1
})
for
{
// allumer la LED quand le bouton est pressé (état bas)
led.
Set(!
button.
Get())
delay(
50
)
}
}
func
delay(
t int64
)
{
time.
Sleep(
time.
Duration(
1000000
*
t))
}
La méthode Get()
retourne un booléen (true
/false
).
TinyGo ne permet pas encore d’activer la résistance de tirage pull-up interne de l’Arduino Uno (ce qui aurait permis d’économiser une résistance externe et la connectique pour lire l’état du bouton).
VIII. PWM : sortie « analogique »▲
Un signal PWM (Pulse Width Modulation ou Modulation en Largeur d’Impulsion) est un signal logique rectangulaire où, sur une période, le pourcentage de temps passé à l’état haut peut être modulé. La valeur moyenne du signal obtenu entre 0 et 5 V est souvent utilisé pour faire varier la luminosité d’une LED ou encore la vitesse d’un moteur à courant continu.
Seules les broches 3, 5, 6, 9, 10 et 11 de l’Arduino Uno (et Nano), dont les connecteurs sont marqués avec le symbole « ~ », peuvent produire un signal PWM en sortie.
Dans l’exemple qui suit, on fait varier progressivement la luminosité de la LED reliée à la broche 9. Le circuit est le même que pour l’exemple Blinky.
package
main
import
(
"
machine
"
"
time
"
)
func
main()
{
machine.
InitPWM()
// initialisation du générateur PWM
led :=
machine.
PWM{
9
}
// configuration de la broche 9
led.
Configure()
// génération du signal
duty,
delta :=
0
,
1024
for
{
led.
Set(
uint16
(
duty))
// définir le rapport cyclique
duty +=
delta
if
duty <
0
||
duty >
65535
{
delta *=
-
1
duty +=
delta
}
delay(
25
)
}
}
func
delay(
t int64
)
{
time.
Sleep(
time.
Duration(
1000000
*
t))
}
La méthode Set() du PWM prend un uint16
en paramètre (0-65 535) et définit la valeur du rapport cyclique (duty cycle). Pour autant, le générateur PWM de la Uno ne fonctionne qu’avec une résolution de 8 bits, et donc certaines valeurs n’auront pas d’effet visible.
Pour l’instant, il n’est pas possible de modifier la fréquence du signal PWM avec TinyGo, ce qui serait pourtant bien pratique pour contrôler certains buzzers piézoélectriques ou servomoteurs.
IX. Entrée analogique▲
Certaines broches de l’Arduino Uno sont reliées à un Convertisseur Analogique-Numérique (CAN) et permettent d’obtenir l’image d’une tension analogique.
Ici, on va utiliser une photorésistance qui produira une tension analogique en fonction de la lumière ambiante. La photorésistance sera reliée à l’entrée analogique A0 afin d’obtenir une image numérique de cette luminosité, et de s’en servir pour allumer ou éteindre la LED reliée à la broche 13.
Un montage avec diviseur de tension sera réalisé sur plaque d’essai, avec une résistance de 10 kΩ :
package
main
import
(
"
machine
"
"
time
"
)
func
main()
{
machine.
InitADC()
// initialisation du CAN
ldr :=
machine.
ADC{
0
}
// configuration de la broche
// équivalent à machine.ADC{machine.ADC0}
ldr.
Configure()
// démarrage du CAN
led :=
machine.
Pin(
13
)
led.
Configure(
machine.
PinConfig{
1
})
for
{
// afficher la valeur via le port série
print
(
ldr.
Get())
// allumer la LED si la valeur est supérieure à un seuil
led.
Set(
ldr.
Get()
>
40000
)
delay(
100
)
}
}
func
delay(
t int64
)
{
time.
Sleep(
time.
Duration(
1000000
*
t))
}
Dans le module machine, les entrées analogiques sont prédéfinies : machine.ADC0 à machine.ADC5.
Comme pour le PWM, la méthode Get() retourne un uin16 entre 0 et 65 535.
Ici, le seuil est fixé à 40 000, mais vous devrez peut-être le modifier en fonction des conditions d’éclairage. Pour consulter les valeurs produites en sortie avec print
()
, vous pouvez utiliser un terminal série comme Tera Term, ou encore le moniteur Série proposé dans l’EDI Arduino.
X. UART : communication série▲
La communication série (UART : universal asynchronous receiver-transmitter) permet de transmettre ou recevoir des données avec un système numérique extérieur, que ce soit un capteur, un autre microcontrôleur ou un PC.
Le code suivant, légèrement modifié par rapport à l’exemple officiel, décrit le fonctionnement de la communication série via le port USB :
package
main
import
(
"
machine
"
"
time
"
)
var
(
uart =
machine.
UART0 // port série matériel
tx =
machine.
UART_TX_PIN // 1, ligne de transmission Tx
rx =
machine.
UART_RX_PIN // 0, ligne de réception Rx
)
func
main()
{
uart.
Configure(
machine.
UARTConfig{
TX:
tx,
RX:
rx})
// équivalent à machine.UARTConfig{9600, 1, 0}
uart.
Write([]
byte
(
"
Echo console enabled. Type something then press enter:
\r\n
"
))
input :=
make
([]
byte
,
64
)
// buffer port série
i :=
0
for
{
if
uart.
Buffered()
>
0
{
data,
_ :=
uart.
ReadByte()
// lecture d’un caractère
switch
data {
case
13
:
// touche Entrée pressée
uart.
Write([]
byte
(
"
\r\n
"
))
uart.
Write([]
byte
(
"
You typed:
"
))
uart.
Write(
input[:
i])
uart.
Write([]
byte
(
"
\r\n
"
))
i =
0
default
:
// autre touche pressée
uart.
WriteByte(
data)
input[
i]
=
data
i++
}
}
time.
Sleep(
10
*
time.
Millisecond)
}
}
Ce code permet de lire un caractère à la fois arrivant sur le port série, et de le stocker dans un tableau d’octets. Quand l’utilisateur appuie sur la touche Entrée (code ASCII = 13), la chaîne de caractères du texte est retournée en sortie vers le port série, puis le tableau est effacé.
TinyGo ne gère pas encore la liaison série par voie logicielle (Software serial).
XI. Et donc, quels genres de projets pouvons-nous réaliser avec une Arduino Uno et TinyGo (à ce jour) ?▲
Certainement beaucoup ! Il y a déjà beaucoup de capteurs et dispositifs peu coûteux qui ne requièrent qu’une simple broche en entrée ou sortie. Voyons quelques exemples…
Exemple 1 : utiliser un pointeur laser, un module avec photorésistance (qui peut être connecté directement, sans montage avec diviseur de tension) et un buzzer piézoélectrique, pour réaliser un simple jeu de tir laser.
Exemple 2 : utiliser un détecteur de présence infrarouge PIR (HC-SR501) et un module relais pour allumer ou éteindre une lumière quand vous passez devant le détecteur.
Exemple 3 : utiliser un capteur d’humidité du sol et un module relais qui actionnera une pompe pour arroser votre plante d’appartement.
Exemple 4 : utiliser un module d’interface moteur CC de type L9110 associé à des modules opto-isolateurs TCRT5000 pour piloter un robot roulant à deux roues motorisées indépendantes et faire un suivi de ligne au sol.
Exemple 5 : utiliser un module accéléromètre ADXL335 (avec une sortie analogique pour des accélérations jusqu’à ±3g) et des LED pour indiquer si le dispositif est posé sur une surface plane.
Exemple 6 : utiliser le langage Go pour envoyer des requêtes à un service Web météo qui retournerait la probabilité de précipitation ou le taux de pollution de l’air, puis qui enverrait les données par le port série vers un dispositif embarqué à quatre afficheurs 7-segments.
Exemple 7 : utiliser un détecteur de fumée qui enverrait ses données depuis l’Arduino vers votre PC. Le programme en Go qui tournerait sur votre PC vous enverrait un courriel grâce au service IFTTT en cas d’alerte.
Maintenant, c’est à vous de jouer !
XII. Note de la rédaction de Developpez▲
Ce tutoriel est une reprise partielle des travaux d’Alan Wang intitulé TinyGo on Arduino Uno : An Introduction (licence C.C BY-NC-SA).
Les programmes en langage Go et les vidéos de démonstration sont ceux d’Alan Wang (mis à part les commentaires des programmes traduits par l’équipe de la rédaction).
Merci à l’équipe de la rédaction de Developpez pour leur travail de traduction et de relecture, en particulier : f-leb et naute.