TinyGo - La programmation en langage Go sur Arduino

Présentation de TinyGo

TinyGo, maintenant officiellement parrainé par Google, est une implémentation du langage Go pour les microcontrôleurs. En utilisant un compilateur basé sur LLVM, TinyGo peut générer un fichier binaire suffisamment compact pour être contenu dans un microcontrôleur, y compris les microcontrôleurs 8 bits AVR avec très peu de mémoire.

4 commentaires Donner une note  l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction à TinyGo

Image non disponible

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 :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ export GOROOT=/usr/local/go

GOPATH définit l’emplacement de votre répertoire de travail. Par exemple, ici, ~/MesProjets/ :

 
Sélectionnez
$ export GOPATH=$HOME/MesProjets

Enfin, le PATH est complété avec l’emplacement des fichiers binaires :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ go version

go version go1.13.8 linux/amd64

Et pour vérifier les variables d’environnement :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ 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 :

 
Sélectionnez
$ 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) :

blinky.go
Sélectionnez
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 :

 
Sélectionnez
$ 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 :

 
Sélectionnez
crw-rw---- 1 root dialout 166, 0 févr. 26 10:49 /dev/ttyACM0

Il faut donc ajouter l’utilisateur à ce groupe :

 
Sélectionnez
$ sudo usermod -a -G dialout <username>

<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 :

 
Sélectionnez
$ 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.

 
Sélectionnez
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.

Image non disponible
blinky2.go
Sélectionnez
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 :

 
Sélectionnez
type PinConfig struct {
    Mode PinMode
}

Mode est en fait un entier uint8 :

 
Sélectionnez
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.

Image non disponible
k2000.go
Sélectionnez
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)

k2000_v2.go
Sélectionnez
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 :

k2000_v3.go
Sélectionnez
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Ω.

Image non disponible
bp.go
Sélectionnez
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.

pwm.go
Sélectionnez
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Ω :

Image non disponible
 
Sélectionnez
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 :

 
Sélectionnez
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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Licence Creative Commons
Le contenu de cet article est rédigé par Alan Wang et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2020 Developpez.com.