Skip to content

📝 Blog

TinyGo - Joystick et boutons

Toute cette série est bien jolie, mais une console de jeu sans moyens d'interaction, c'est pas génial.
Je vous propose aujourd'hui de voir comment contrôler le joystick et les boutons de la Pygamer.

Je rappelle ici le disclaimer de l'article sur les fontes : la partie graphique va nécessiter un peu de travail, l'usage de tinyfont ou tinydraw ne fournira pas les performances nécessaires. Des projets ont tenté des approches, il faudra aller les analyser pour voir ce qui est faisable. Je pense au projet GoBadge, destiné au Pybadge, qui a plein de points communs avec la Pygamer, ou au projet Xship qui propose un jeu sur Pygamer, mais je ne l'ai pas encore essayé.
Merci d'ailleurs à Daniel Esteban et Jean-Yves Pellé pour ces 2 projets qui m'ont permis de comprendre beaucoup de choses.

Mais sans plus attendre, le code du jour :

package main

import (
    "image/color"
    "machine"
    "strconv"

    "tinygo.org/x/drivers/st7735"
    "tinygo.org/x/tinyfont"
    "tinygo.org/x/tinyfont/freemono"
)

var black = color.RGBA{0, 0, 0, 0}
var red = color.RGBA{255, 0, 0, 255}
var gray = color.RGBA{200, 200, 200, 255}

func main() {
    buttons := NewButtons()
    buttons.Configure()

    joystick := NewJoystick()
    joystick.configure()

    display := st7735.New(machine.SPI1, machine.TFT_RST, machine.TFT_DC, machine.TFT_CS, machine.TFT_LITE)
    machine.SPI1.Configure(machine.SPIConfig{
        Frequency: 16000000,
        SCK:       machine.SPI1_SCK_PIN,
        SDO:       machine.SPI1_SDO_PIN,
        SDI:       machine.SPI1_SDI_PIN,
    })
    display.Configure(st7735.Config{Rotation: st7735.ROTATION_90})
    display.FillScreen(black)

    var buttons_pressed = ""
    var x_axis uint16 = 0
    var y_axis uint16 = 0
    for {
        x_axis = joystick.getX()
        y_axis = joystick.getY()
        buttons_pressed = ""
        pressed, _ := buttons.Read8Input()
        if pressed != 0 {
            println(pressed)
        }
        if pressed&machine.BUTTON_A_MASK > 0 {
            buttons_pressed += "BTN A"
            println("BTN A")
        }
        if pressed&machine.BUTTON_B_MASK > 0 {
            buttons_pressed += "BTN B"
            println("BTN B")
        }
        if pressed&machine.BUTTON_SELECT_MASK > 0 {
            buttons_pressed += "SELECT"
            println("SELECT")
        }
        if pressed&machine.BUTTON_START_MASK > 0 {
            buttons_pressed += "START"
            println("START")
        }

        display.FillScreen(black)
        tinyfont.WriteLine(&display, &freemono.Bold9pt7b, 10, 20, strconv.Itoa(int(x_axis)), gray)
        tinyfont.WriteLine(&display, &freemono.Bold9pt7b, 10, 50, strconv.Itoa(int(y_axis)), gray)
        tinyfont.WriteLine(&display, &freemono.Bold9pt7b, 10, 80, buttons_pressed, gray)
    }
}

Cette fois, ça fait un fichier plus conséquent et en plus il y a un fichier à côté pour aider à simplifier la gestion des boutons et du joystick.
Je vais tâcher d'expliquer au maximum tout ce qui se passe.

Commençons par les boutons.

buttons := NewButtons()
buttons.Configure()

Vous voyez comme c'est simple ? ;)
Le code nécessaire se trouve bien sûr dans pygamer.go et une version brute de ce qui est appellé serait en fait :

var buttons = shifter.New(shifter.EIGHT_BITS, machine.BUTTON_LATCH, machine.BUTTON_CLK, machine.BUTTON_OUT)

Vous pouvez regarder le code entier, mais à part déclarer la fonction NewButtons et gérer le retour d'un objet device que nous stockons dans la variable buttons, il n'y a rien de plus.
Vous voyez qu'il s'agit d'utiliser le driver tinygo.org/x/drivers/shifter qui va gérer le composant qui permet d'accédr à l'état des boutons de la Pygamer.
Ce type de composant permet de ne pas monopoliser autant de broches du micro contrôleur qu'il y a de boutons.

Il suffit alors de récupérer l'état de ce composant :

pressed, _ := buttons.Read8Input()

La variable contient alors une information qu'il faut décoder à l'aide de masques logiques. Je vous laisse faire des recherches sur les registres à décalages ( = Shift registers => shifter) mais il y a un article assez complet sur Electronique amateur.

if pressed&machine.BUTTON_B_MASK > 0 {...

C'est ce que l'on fait par cette ligne qui sert ici à savoir si le bouton B est appuyé.
Notez que l'on peut tester l'appui simultanné de plusieurs boutons, la variable pressed contient alors une valeur cumulant les différents bits correspondants.

Pour la partie joystick, nous allons aussi décortiquer ce que signifie les lignes :

joystick := NewJoystick()
joystick.configure()

Le code brut si on se passait du fichier pygamer.go serait alors :

type joystickStruct struct {
    x machine.ADC
    y machine.ADC
}
// équivalent à joystick := NewJoystick()
joystick := joystickStruct{x: machine.ADC{machine.JOYX}, y: machine.ADC{machine.JOYY}}

// équivalent à joystick.configure
joystick.x.Configure(machine.ADCConfig{})
joystick.y.Configure(machine.ADCConfig{})

Pour rassembler la logique, on utilise une structure qui contiendra en fait les 2 axes du joystick.
Ces 2 axes sont des composants générants des valeurs analogiques qui ont ensuite besoin d'êtres converties en valeurs numériques pour que nous puissions les utiliser.
Cela passe par une couche d'abstraction matérielle ADC (Analog to Digital Conversion).
machine.JOYX et machine.JOYY sont les 2 objets définis dans TinyGo pour Pygamer pour représenter les 2 axes.

Nous pouvons ensuite lire les valeurs de ces axes :

x_axis = joystick.getX()
y_axis = joystick.getY()

Ce qui correspond au code :

x_axis = joystick.x.Get()
y_axis = joystick.y.Get()

Le reste du code est ma tentative pour afficher les informations récupérées.
Vous pouvez constater qu'il y a un phénomène de clignotement qu'il faudra gommer par la suite par un usage différent des ressources graphiques.

Sorry, your browser doesn't support embedded videos.

Vous en avez maintenant l'habitude, cet exemple est disponible dans le repository dédié aux exemples TinyGo.

Merci, n'hésitez pas à me faire parvenir vos remarques par email ou par le biais d'issues sur le repository.

TinyGo - Les fontes

Utiliser les Neopixels de la Pygamer peut déjà apporter un moyen de communication visuelle mais il faut pouvoir écrire des textes sur l'écran.

Pour cela, TinyGo propose un module appellé TinyFont.
Ce projet fourni même un outil pour générer vos fontes au format utilisable par TinyFont.
Ici, je me contenterai d'utiliser les fontes déjà fournies par le projet afin de ne pas rajouter de complexité supplementaire.

Avant de se lancer, j'ajouterai que nous entrons là dans un domaine manipulant les ressources graphiques de la Pygamer et que rapidement il faudra creuser le fonctionnement du driver. Mais nous y reviendrons ultérieurement.

Sans plus attendre, comme à mon habitude, je vous propose le code utilisé pour cet article :

package main

import (
    "image/color"
    "machine"
    "time"

    "tinygo.org/x/drivers/st7735"
    "tinygo.org/x/tinyfont"
    "tinygo.org/x/tinyfont/freemono"
    "tinygo.org/x/tinyfont/freesans"
)

var black = color.RGBA{0, 0, 0, 0}
var red = color.RGBA{255, 0, 0, 255}
var green = color.RGBA{0, 255, 0, 255}

func main() {
    display := st7735.New(machine.SPI1, machine.TFT_RST, machine.TFT_DC, machine.TFT_CS, machine.TFT_LITE)
    machine.SPI1.Configure(machine.SPIConfig{
        Frequency: 16000000,
        SCK:       machine.SPI1_SCK_PIN,
        SDO:       machine.SPI1_SDO_PIN,
        SDI:       machine.SPI1_SDI_PIN,
    })
    display.Configure(st7735.Config{Rotation: st7735.ROTATION_90})

    for {
        display.FillScreen(black)
        tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 20, "No rotation", red)
        time.Sleep(1 * time.Second)

        display.FillScreen(black)
        tinyfont.WriteLineRotated(&display, &freemono.Bold9pt7b, 10, 10, "HELLO", green, tinyfont.ROTATION_90)
        time.Sleep(1 * time.Second)

        display.FillScreen(black)
        tinyfont.WriteLineRotated(&display, &freemono.Bold9pt7b, 100, 50, "Hello", red, tinyfont.ROTATION_180)
        time.Sleep(1 * time.Second)

        display.FillScreen(black)
        tinyfont.WriteLineRotated(&display, &freemono.Bold9pt7b, 120, 100, "HELLO", green, tinyfont.ROTATION_270)
        time.Sleep(1 * time.Second)
    }
}

Tout le début a été vu dans l'article de base sur la manipulation de l'écran.
J'ai apporté une modification à la configuration de l'écran.

display.Configure(st7735.Config{Rotation: st7735.ROTATION_90})

Par défaut, l'écran de la Pygamer est géré par le chip st7735 comme si vous le teniez avec le joystick en haut. Les librairies pour cette console en C ou en CircuitPython ont fait le choix de fournir un driver qui opère cette rotation d'initialisation, la rendant transparente pour l'utilisateur.
Cela peut être une suggestion de modification pour le driver st7735 dans le cas de la Pygamer, je vous tiendrais au courant si je me lance à proposer une PR dans ce sens.

La partie concernant les fontes commence par les imports des éléments utiles comme TinyFont lui même, mais également 2 fontes pour que vous ayez déjà le choix.

"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freemono"
"tinygo.org/x/tinyfont/freesans"

L'affichage de texte se fera dans la boucle for où j'ai alterné 3 messages, entrecoupé d'un effacement de l'écran.
Pour effacer l'écran justement, j'ai utilisé la fonction :

display.FillScreen(black)

Je me contente donc de remplir l'écran de pixels noirs pour effacer le texte précédent.
Comme nous allons ensuite tracer les lettres, ce n'est surement pas la méthode que je conseillerai pour un jeu, l'usage d'un buffer mémoire sera à envisager, et nous le ferons surement dans un prochain article.
L'affichage des textes se fera avec les fonctions tinyfont.WriteLine et tinyfont.WriteLineRotated

tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 20, "No rotation", red)

tinyfont.WriteLine permet ici d'afficher le message en respectant l'orientation de l'écran configurée. Vous noterez l'usage de l'appel de fonction avec des références (symbole &)
Si vous souhaitez comprendre ce point précis, je vous conseille un peu de lecture avec un chapitre sur les pointeurs, notament le 2.4 qui montre l'utilisation des pointeurs dans le cadre de paramètres de fonction.

Vous voyez donc que l'on confie à cette fonction la variable display gérant l'écran.
Cela servira en interne pour afficher les pixels des lettres.
Le second paramètre indique la fonte qui sera utilisée. En l'occurence, il s'agit de la famille de fonte freesans dans son format Regular9pt7b qui indique la taille et les détails tels que préparés par l'utilitaire TinyFontGen
Viennent ensuite les paramètres x et y permettant de positionner le texte.
Le paramètre suivant est le texte à afficher, suivi de la couleur souhaitée.

La seconde fonction utilisée est tinyfont.WriteLineRotated :

tinyfont.WriteLineRotated(&display, &freemono.Bold9pt7b, 10, 10, "HELLO", green, tinyfont.ROTATION_90)

Le comportement est identique, avec l'ajout du dernier paramètre permettant d'indiquer l'orientation du texte par rapport à celle de l'écran.

Bien entendu, il ne vous reste plus qu'à flasher la Pygamer :

tinygo flash -target=pygamer

Et voici ce que cela donne :

Sorry, your browser doesn't support embedded videos.

Bien entendu, cet exemple est disponible dans le repository dédié aux exemples TinyGo.
N'hésitez pas à essayer vos propres paramètres, jouez avec les couleurs, les positions et orientations. Je serai ravi de voir vos essais, partagez les moi.

Merci, n'hésitez pas à me faire parvenir vos remarques par email ou par le biais d'issues sur le repository.

TinyGo - Show time !

Après les deux premiers articles, vous êtes maintenant capables de manipuler l'écran et la led arrière de la Pygamer.

Mais est ce que ce ne serait pas mieux de pouvoir faire un petit show lumineux avec les 5 Neopixels en façade ?
Voyons voir ce que l'on peut faire.

Comme d'habitude, je vous donne le code tout de suite pour le commenter ensuite.

package main

import (
    "image/color"
    "machine"
    "time"

    "tinygo.org/x/drivers/ws2812"
)

var (
    black = color.RGBA{R: 0, G: 0, B: 0}
    red   = color.RGBA{R: 255, G: 0, B: 0}
    green = color.RGBA{R: 0, G: 255, B: 0}
    blue  = color.RGBA{R: 0, G: 0, B: 255}
)

func main() {
    // color intensity does not seems to work but you got the idea ;)
    reds := make([]color.RGBA, 5)
    reds[0] = color.RGBA{R: 50, G: 0, B: 0}
    reds[1] = color.RGBA{R: 100, G: 0, B: 0}
    reds[2] = color.RGBA{R: 150, G: 0, B: 0}
    reds[3] = color.RGBA{R: 200, G: 0, B: 0}
    reds[4] = color.RGBA{R: 255, G: 0, B: 0}

    colors := make([]color.RGBA, 5)

    neopixels := ws2812.New(machine.NEOPIXELS)
    neopixels.Pin.Configure(machine.PinConfig{Mode: machine.PinOutput})

    for {
        for i := range [5]int{0, 1, 2, 3, 4} {
            clear(colors)
            colors[i] = reds[i]
            neopixels.WriteColors(colors)
            time.Sleep(time.Millisecond * 500)
        }
    }
}

func clear(colors []color.RGBA) {
    colors[0] = black
    colors[1] = black
    colors[2] = black
    colors[3] = black
    colors[4] = black
}

Pour manipuler les Neopixels, nous allons utiliser le driver tinygo.org/x/drivers/ws2812 que vous voyez dans les imports.

neopixels := ws2812.New(machine.NEOPIXELS)
neopixels.Pin.Configure(machine.PinConfig{Mode: machine.PinOutput})

De manière assez similaire à la Led, nous déclarons ensuite un objet pour nos Neopixels et nous configurons la broche correspondante.
Une fois de plus, le module machine nous fournit la référence qu'il nous faut.

Pour manipuler les Neopixels, nous utiliserons ici la fonction WriteColors. Cette fonction a besoin d'un paramètre qui sera un tableau avec chaque couleur de chaque Led.
Ce tableau est créé sur la ligne :

colors := make([]color.RGBA, 5)

Le tableau contiendra donc 5 couleurs RGBA, une pour chaque Neopixel de la Pygamer.

for i := range [5]int{0, 1, 2, 3, 4} {
    clear(colors)
    colors[i] = reds[i]
    neopixels.WriteColors(colors)
    time.Sleep(time.Millisecond * 500)
}

Cette boucle est alors en charge de s'assurer que chaque Neopixel est éteinte, puis on affecte la couleur rouge à une Neopixel à la fois, la boucle passant de l'une à l'autre.
Je laisse volontairement mon code ainsi, il n'est pas tout à fait complet par rapport à ce que je visais. Je cherchais à reproduire un effet "K2000" ou "Cylon" mais je me suis aperçu que l'intensité ne semblait pas être prise en charge par le driver actuel.
Cela fera l'objet d'un prochain article et d'une mise à jour du code si je parviens à résoudre ce point.

Voilà, il ne vous reste plus qu'à flasher la Pygamer :

tinygo flash -target=pygamer

Profitez du spectacle !

Sorry, your browser doesn't support embedded videos.

Bien entendu, cet exemple est disponible dans le repository dédié aux exemples TinyGo.

Merci, n'hésitez pas à me faire parvenir vos remarques par email ou par le biais d'issues sur le repository.

TinyGo - Allumons l'écran de la Pygamer

Maintenant que nous avons vu comment mettre en place un environnement qui permet de coder avec TinyGo pour la Pygamer, voyons ce que l'on peut faire de l'écran de cette console.

Vous l'avez peut être vu dans la documentation d'installation, mais il existe des modules rapidement essentiels au développement avec Tinygo.
Ces modules contiennent le code nécessaire pour piloter les différents aspects matériels des différentes plateformes.

Le projet TinyGo Drivers rassemble les principaux drivers dont vous aurez besoin.
Vous pouvez installer tout le nécessaire en suivant les informations du Readme, le tout se résumant à une commande :

go get tinygo.org/x/drivers

Pour manipuler l'écran de la Pygamer, la documentation TinyGo nous indique qu'il faut utiliser le driver pour le composant ST7735.
C'est ce que nous allons faire et sans plus attendre, le code du jour :

package main

import (
    "image/color"
    "machine"
    "time"

    "tinygo.org/x/drivers/st7735"
)

var red = color.RGBA{255, 0, 0, 255}
var green = color.RGBA{0, 255, 0, 255}

func main() {
    display := st7735.New(machine.SPI1, machine.TFT_RST, machine.TFT_DC, machine.TFT_CS, machine.TFT_LITE)
    machine.SPI1.Configure(machine.SPIConfig{
        Frequency: 16000000,
        SCK:       machine.SPI1_SCK_PIN,
        SDO:       machine.SPI1_SDO_PIN,
        SDI:       machine.SPI1_SDI_PIN,
    })
    display.Configure(st7735.Config{})

    i := 0
    for {
        display.FillScreen(green)
        time.Sleep(1 * time.Second)
        display.FillScreen(red)
        time.Sleep(1 * time.Second)
        i++
    }
}

Eliminons tout d'abord les lignes de codes qui sont nécessaires mais qui ne vous poseront pas de souci une fois copiées.
Le module est ajouté dans les import :

"tinygo.org/x/drivers/st7735"

Vous aurez alors à disposition tous les outils prévus par ce module pour allumer, contrôler des pixels...
Il suffit d'instancier un objet pour manipuler l'écran :

display := st7735.New(machine.SPI1, machine.TFT_RST, machine.TFT_DC, machine.TFT_CS, machine.TFT_LITE)
machine.SPI1.Configure(machine.SPIConfig{
        Frequency: 16000000,
        SCK:       machine.SPI1_SCK_PIN,
        SDO:       machine.SPI1_SDO_PIN,
        SDI:       machine.SPI1_SDI_PIN,
    })
display.Configure(st7735.Config{})

Les paramètres nécessaires correspondent aux broches impliquées dans le contrôle du composant ST7735.
Ce module peut être utilisé avec n'importe quel micro-contrôleur capable de piloter ce type d'écran, mais dans notre cas, le module machine de la Pygamer fournit des constantes explicites pour cela.
Il reste ensuite à paramétrer correctement le port SPI utilisé.
Là encore, les constantes existantes dans le module machine seront d'une grande aide.
Il faut ensuite s'assurer que l'objet display est bien configuré.

A partir de là, nous allons vérifier que nous avons bien la maîtrise de l'écran de manière simple : nous alternerons entre un écran vert et un écran rouge, toutes les secondes. Cela nous assurera du bon fonctionnement de notre configuration.
La fonction FillScreen permet un remplissage de l'écran basé sur une couleur RGBA.

Il n'y a plus qu'à lancer le flash comme précédemment :

tinygo flash -target=pygamer

Et voilà le résultat !

Sorry, your browser doesn't support embedded videos.

Comme toujours, vous trouverez cet exemple dans le repository TinyGo Examples.

A bientôt pour la suite avec une fois encore plein de lumières colorées ;)

TinyGo - et si on codait une Pygamer

Tout comme je l'avais fait l'année dernière, je suis parti en vacances avec mon iPad mais cette fois avec quelques idées de code autour du language Rust.
J'ai appris pas mal de choses avec ce language mais il me manquait cette petite touche de fun que j'avais ressentie avec Go en 2020.

Et puis sur ma dernière semaine de congés, en changeant de lieu de villégiature, j'ai emporté de quoi essayer à nouveau la version embarquée de Go, connue sous le petit nom de TinyGo.
J'avais pris une série de cartes que j'ai, une micro:bit évidemment, mais également des M4 de Arduino et ma Pygamer.

Très rapidement, j'ai pu faire de chouettes progrès mais qui ont nécessité de fouiller pas mal Internet, Slack et Discord.
L'idée est de vous partager tout ça en une série d'articles, accompagnés d'un repository sur Gitlab.
Je remercie au passage Aurélie Vache qui m'a aidé à garder le focus, parfois sans le savoir, moi qui m'éparpille parfois, grâce à ces petits messages et ses expériences avec la Gameboy. (on en reparlera, promis) et ses articles sur Go.

Mais sans plus attendre, en avant pour le monde de TinyGo.

Je vous conseille de suivre simplement leur Getting Started qui m'a permis d'avoir un environnement fonctionnel autant sur mon Macbook que ma machine Linux.
J'ajouterai que j'ai essentiellement utilisé VSCode qui a de bonnes extensions pour Go qui m'ont suffit, le reste se passant dans le terminal, intégré c'est pratique.

Pour ce qui est de la Pygamer, la documentation de base se trouve dans la section microcontroller du site de TinyGo. Nous y reviendrons dans un prochain article, mais la documentation est très légère, les exemples quasi inexistants.
Comme ce n'est pas la première fois que je m'en fait la remarque, j'ai décidé d'essayer d'apporter ma petite pierre en écrivant cette série d'article, mais surtout en publiant un repository rassemblant des exemples très réduits sur ce qu'ils abordent pour permettre à chacun de se lancer.

Sans plus attendre, lançons nous dans notre premier code, le Hello World des micro contrôleurs, le clignotement d'une led ;)

Le code en lui même est assez simple, mais je vous conseille de prendre un tout petit peu de temps pour avoir les bases minimales du language Go.

package main

import (
    "machine"
    "time"
)

func main() {
    led := machine.LED
    led.Configure(
        machine.PinConfig{Mode: machine.PinOutput} )
    for {
        led.Low()
        time.Sleep(time.Millisecond * 500)

        led.High()
        time.Sleep(time.Millisecond * 500)
    }
}

Je tacherai de conserver des exemples avec le moins de code possible, en me concentrant sur une fonctionnalité à la fois.

Ici, nous ferons appel à machine.LED qui correspond à la broche D13 si l'on lit la documentation du module machine pour la Pygamer.
Nous utiliserons ensuite les fonctions pour alterner l'état de cette broche entre l'état low avec led.Low()) et high avec led.High(). Cela correspondra à une alternance entre 0V et 3,3V sur la broche D13 qui correspond sur la Pygamer à une Led au dos, à côté de la prise USB et clairement indiquée D13.

Il ne restera plus qu'à compiler et flasher ce code sur la Pygamer avec la commande indiquée sur la page de documentation :

tinygo flash -target=pygamer

Je lance cette commande en étant dans le répertoire où se trouve mon code dans un fichier main.go.
La Pygamer va passer par un écran intermédiaire indiquant qu'elle est en mode flashable :

Puis l'écran redevient noir et la Led au dos se met à clignoter. Il s'agit de la Led rouge sur la photo, la Led orange est celle indiquant la charge en cours.

Il est possible que le premier flash ne se passe pas, auquel cas je vous invite à suivre la section troubleshooting de la documentation que je résume ici :

  • double appui sur la touche reset, pas trop vite.
  • la Pygamer apparait comme une clé USB appelée Pygamerboot et l'écran est celui montré plus haut.
  • vous pouvez flasher.

Vous voulà prêt.e.s à embarquer sur le navire TinyGo !
Je met tout le code que j'écris pour ces articles à disposition sur un repository Gitlab. Comme vous le verrez, j'ai d'autres exemples à votre disposition et ils feront l'objet de prochains articles.