Journal Prise en main de la carte Longan Nano RISC-V de Sipeed

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
34
23
déc.
2020

Sommaire

Nous avons déjà parlé du RISC-V dans un article précédent avec la carte HiFive dotée d'un microcontrôleur pas si intéressant. Celui présenté cette fois-ci semble beaucoup plus standard : il fait le plein de périphériques et de mémoire Flash embarquée.

Présentation de l'architecture du RISC-V

Nous avons déjà un peu présenté l'historique du RISC V dans l'article précédent. Essayons maintenant de découvrir le modèle de programmation ; cela nous servira plus tard lors de débogages et développement bas niveau.

La fondation RISC a rédigé deux spécifications de cette nouvelle ISA (Instruction Set Architecture) :
- Un document général à l'accès non-privilégié
- Les spécifications des accès privilégiés

Qu'est ce que c'est que l'accès privilégié ? Il faut savoir que les processeurs de type "PC", qui sont voués à être utilisés avec un gros système d'exploitation type Linux/Windows/Mac disposent de protection d'accès physique. En gros, c'est le processeur qui s'assure qu'une instruction assembleur peut être exécutée. Cela permet d'offrir une séparation entre les processus systèmes et utilisateur. Sous Linux par exemple, un utilisateur privilégié est 'root'.

Intéressons-nous à cette première documentation qui décrit le coeur RISC-V minimal, le RV32I, ainsi que toutes les extensions optionnelles. En effet, l'architecture décrite permet de créer des processeurs de 32-bits, 64-bits ou 128-bits, c'est à dire du petit microcontrôleur au processeur dopé pour serveurs ou calculs scientifiques (c'est à l'origine le but de cette architecture créée à l'université de Berkley).

Cette première documentation nous montre donc le modèle de programmation, la liste des registres :

image

L'architecture est donc assez simple, j'aime bien. Nous avons 32 registres à usage générique, tout du moins si on est seul au monde et que l'on ne souhaite pas développer quelque chose de "portable". Typiquement un logiciel développé en assembleur. Bien entendu, il s'agit ici des registres entiers. L'architecture RISC-V dispose de beaucoup d'extensions optionnelles dont par exemple l'extension offrant un jeu de registres flottants (IEEE 754).

Rappel de la codification des extensions :

  • Coeur RV32 'I' (A load-store ISA with 32, 32-bit general-purpose integer registers) avec les options :
    • M : Integer Multiplication and Division
    • A : Atomics
    • C : 16-bit Compressed Instructions
    • F : Single-Precision Floating-Point
    • D : Double-Precision Floating-Point
    • Q : Quad-Precision Floating-Point

Au moment de l'écriture de cet article, plusieurs extensions supplémentaires sont prévues mais non encore décites (Bit manipulation, Dynamically Translated Languages, Vector Operations …). On le voit, cette architecture est vouée à évoluer dans les prochaines années.

Enfin, la spécification décrit également le langage assembleur et spécifie une convention d'appel et de comportement (ABI, Application Binary Interface) des registres, ceci étant nécessaires pour préciser quels registres sont utilisés pour les arguments des fonctions, les divers pointeurs, adresses de retour etc.

image

Chaque cœur RISC-V dispose de son propre contrôleur d'interruption appelé CLIC (Core Local Interrupt Controller), il est dit logical car rattaché à un seul coeur ce qui peut avoir sens dans un composant multi-coeurs.

Gigadevice et Nuclei

D'après mes recherches et ce que j'en ai compris, l'IP du coeur RISC-V est pris chez la société Nuclei qui dispose d'un catalogue complet de coeurs (https://www.nucleisys.com/product/rvipes/n200/). Le notre, c'est celui-ci :

image

Ce qui va nous intéresser ici est la présence d'un timer dans le coeur même. Typiquement, l'usage sera pour séquencer un système ; chez ARM, ils l'ont standardisé et l'ont appelé SystTick. Je suis un peu déçu d'ailleurs sur ce point, car avoir un timer standardisé permet de passer du code d'un microcontrôleur à l'autre sans ce soucier de ce point.

Pour la performance, le coeur s'annonce plus véloce et moins gourmand que son principal concurrent, la famille Cortex-M :

image

Bon c'est une documentation constructeur, il faut en prendre et en laisser car on ne connaît pas les conditions de test. En réel, cela dépendera beaucoup de la performance de la Flash embarquée au MCU et de bien d'autres paramètres.

Un peu d'assembleur !

Essayons de tâter un peu plus profondément l'architecture. Pour s'amuser, nous allons utiliser le simulateur en ligne BRISC. On copie le programme ci-dessous qui réalise une petite boucle d'incrémentation d'un registre. Retirez les commentaires, ils ne passent pas sur le simulateur :

    .file   "example.c"
    .option nopic
    .text
    .align  2
    .globl  example

main:
    li a0, 3 # chargement immédiat d'une valeur
    li a1, 10 # a1 contiendra notre valeur finale de sortie
loop:
    addi a0, a0, 1   # a0 = a0 + 1
    bne a0, a1, end  #  Branch if Not Equal
    j loop # sinon on saute 'Jump'

end:

Il est possible d'avancer pas à pas et de voir graphiquement le changement des différents registres.

image

Si vous voulez aller plus loin, Western Digital diffuse un tutorial complet d'assembleur sur Youtube en utilisant la carte HiFive : https://www.youtube.com/watch?v=KLybwrpfQ3I&list=PL6noQ0vZDAdh_aGvqKvxd0brXImHXMuLY.

N'oubliez pas le petit émulateur développé par le génie Fabrice Bellard : https://bellard.org/tinyemu/ qui fournit en plus une image Linux prête à l'emploi !

Présentation de la carte électronique

La société GigaDevice a donc conçu ce microcontrôleur à base du coeur libre RISC-V dans une version RV32IMAC. La version que nous allons utiliser ici est la référence GD32VF103CBT6 qui embarque 128Ko de Flash et 32Ko de RAM. La fréquence du processeur monte à 108 Mhz et les périphériques sont légions : USART, I2C, SPI, CAN, USB, I2S, ADC 12 bits. Bref, le minimum syndical ! Les GPIO des périphériques sont remappables, attention toutefois, dans une certaine mesure (la liste est dans la datasheet).

Que ce soit dans le code source des drivers et le nommage, l'inspiration STM32 est totale et c'est tant mieux car j'adore les STM32. J'ai lu quelque part que le composant était même totalement compatible broche à broche avec certaines versions de STM32.

image

La carte que nous allons utiliser est la Longan Nano et coûte 5 Euros, avec en option un écran LCD. Ce qui rend le tout assez sympa.
image

image

Sur une des extrémités, vous trouverez un port USB-C et à l'autre bout un connecteur JTAG : merci d'y avoir pensé, c'est assez rare pour le souligner. À côté du JTAG, sur le même connecteur, on y trouve l'UART0 qui va nous servir comme organe de débogage (même si a priori on peut s'en servir pour programmer la carte à la manière d'Arduino).

Point d'entrée pour vos documents :
* https://longan.sipeed.com/en/
* https://dl.sipeed.com/LONGAN/Nano/DOC/

Arsenal de développement

Pour commencer, nous allons utiliser Visual Studio Code avec l'extension PlatformIO : à la manière d'Arduino, il vous simplifie le démarrage rapide sur une nouvelle carte en permettant de coder et programmer votre premier bout de code très facilement, en quelques clics ! Ce genre d'outils est idéal pour essayer une carte, même si je pense qu'il faut s'en éloigner si on veut produire du code industriel qui se vend, notamment pour des problématiques de maintenance.

Quoiqu'il en soit, une connexion USB-C suffit pour envoyer votre code dans le micro. La manipulation est la suivante : maintenez le bouton reset et le bouton boot0 situés sur la carte, puis relachez le reset : le bootloader intégré sera exécuté ce qui créera un dispositif de type DFU sous Linux. Cliquez ensuite sur "upload" et c'est parti !!

Comme éditeur de code : restons sur Visual Studio Code, l'intégration avec Segger est possible. Nous tenterons également QtCreator qui fournit une interface C/C++ exemplaire tant au niveau de l'édition de code que du débogage. Il est réactif !

Au niveau de la librairie fournie par GigaDevice : c'est un sans fautes, pour le moment. Là encore on sent l'inspiration ST, ici vous êtes en terrain connu. La librairie est toute simple, légère, de fines fonctions d'abstraction des registres des périphériques. Messieurs les autres fondeurs, merci de vous en inspirer et arrêtez avec vos interfaces graphiques de génération de code, c'est horrible. Un dossier d'exemples est fourni, bref normalement nous avons tout ce qu'il faut pour commencer facilement.
image

Point d'entrée pour le tutorial PlatformIO : https://docs.platformio.org/en/latest/boards/index.html#gigadevice-gd32v

Si vous voulez obtenir un compilateur déjà pré-compilé pour vos développements, Nuclei en fournit un ici : https://www.nucleisys.com/download.php.

Le Hello World de l'embarqué : Blinky

Le but est ici de faire clignoter une LED. La carte dispose d'une LED RGB tricolore câblée comme ceci :
image

Le PlateformIO dispose d'un exemple tout fait permettant de faire clignoter la LED embarquée. Pour réaliser un délai entre l'extinction et l'allumage de la LED, l'exemple utilise une lecture bloquante du timer embarqué dans chaque coeur RISC. Attention donc, c'est utile mais cela bloque tout et dans un contexte multi-tâches on utilisera d'autres moyens non bloquants.

#include "gd32vf103.h"
#include "systick.h"
#include <stdio.h>


/* BUILTIN LED GREEN*/
#define LED_PIN BIT(1)
#define LED_GPIO_PORT GPIOA
#define LED_GPIO_CLK RCU_GPIOA

void longan_led_init()
{
    /* enable the led clock */
    rcu_periph_clock_enable(LED_GPIO_CLK);
    /* configure led GPIO port */ 
    gpio_init(LED_GPIO_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, LED_PIN);

    GPIO_BC(LED_GPIO_PORT) = LED_PIN;
}

void longan_led_on()
{
    GPIO_BC(LED_GPIO_PORT) = LED_PIN;
}

void longan_led_off()
{
    GPIO_BOP(LED_GPIO_PORT) = LED_PIN;
}

int main(void)
{
    longan_led_init();
    init_uart0();

    while(1){
        /* turn on builtin led */
        longan_led_on();
        delay_1ms(1000);
        /* turn off uiltin led */

        longan_led_off();
        delay_1ms(1000);
    }
}

Comme le montre la documentation, il va falloir créer une règle Linux pour pouvoir programmer le composant :

sudo nano /etc/udev/rules.d/90-longan-nano.rules
ATTRS{idVendor}=="28e9", ATTRS{idProduct}=="0189", MODE="0666"
udevadm control --reload-rules && udevadm trigger

Hello World plus poussé : L'UART

Cette fonction sera autrement plus pratique : lors de vos développements embarqués, avoir une console série qui affiche le bon déroulement de votre programme est indispensable. Cela doit être une des premières choses à prévoir dans votre développement car cela vous servira tout le temps !

Donc là rien de bien compliqué : il faut d'abord repérer quelle broche nous allons utiliser. Sur le longan, le connecteur d'extrémité contient le brochage Tx/Rx typiquement mis là pour cet usage.

image

Le code d'initialisation est en deux parties :
- D'une part nous allons utiliser le mode alternatif de la broche PA9, c'est-à-dire non pas en GPIO mais en UART
- D'autre part la configuration du module UART proprement dit à la fréquence voulue :

static void init_uart0(void)
{
   // enable GPIO clock 
    rcu_periph_clock_enable(RCU_GPIOA);
    gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);

   // enable USART0 clock 
   rcu_periph_clock_enable(RCU_USART0);  
   // configure USART0
   usart_deinit(USART0);
   usart_baudrate_set(USART0, 115200U);
   usart_word_length_set(USART0, USART_WL_8BIT);
   usart_stop_bit_set(USART0, USART_STB_1BIT);
   usart_parity_config(USART0, USART_PM_NONE);
   usart_hardware_flow_rts_config(USART0, USART_RTS_DISABLE);
   usart_hardware_flow_cts_config(USART0, USART_CTS_DISABLE);
   usart_receive_config(USART0, USART_RECEIVE_ENABLE);
   usart_transmit_config(USART0, USART_TRANSMIT_ENABLE);
   usart_enable(USART0);
}

// retarget the C library printf function to USART0
int _put_char(int ch) // used by printf
{
     usart_data_transmit(USART0, (uint8_t) ch );
     while (usart_flag_get(USART0, USART_FLAG_TBE) == RESET){
     }
     return ch;
}

Notez ici que la fonction _put_char est re-définie : c'est cette fonction qui est au bout d'un printf et se charge d'envoyer un caractère uniquement quelque part : par défaut je pense qu'elle ne fait rien, car nous sommes dans un environnement embarqué. Nous allons envoyer tout caractère reçu vers l'UART.

Et enfin la fonction principale est modifiée en ajoutant notre initialisation et l'appelle du printf();

int main(void)
{
    longan_led_init();
    init_uart0();

    while(1){
        printf("ON\n");
        /* turn on builtin led */
        longan_led_on();
        delay_1ms(1000);
        /* turn off uiltin led */

        printf("OFF\n");
        longan_led_off();
        delay_1ms(1000);
    }
}

Maintenant munissez-vous d'un contrôleur USB-série quelconque pour relier le port série du Longan au PC :

image

Ouvrez un terminal série sur votre port série créé par la puce FTDI et observez !

image

Les différentes fréquences et le timer

Ok alors jusqu'à maintenant, nous avons copié-collé un peu de code pris à droite et à gauche. Sauf que nous ne maîtrisons pas grand chose sur la fréquence de fonctionnement. A priori, le délai bloquant fourni par la librairie standard fonctionne vu la fréquence de clignotement de la LED, mais on ne sait pas à quelle fréquence tourne le CPU.

On affiche la fréquence en récupérant la valeur de la variable globale SystemCoreClock qui est initialisée au démarrage, avant le main().

printf("[OST] Starting with CPU=%d\n", (int)SystemCoreClock);

Voici le schéma de la PLL, le CPU est donc bien cadencé à la fréquence maximale ici, soit 108 MHz.

image

Notons que le timer SysTick offert par le coeur Nuclei est lui divisé par 4 en entrée. Le meilleur moyen de vérifier si la fréquence CPU est correcte est de faire bagoter un GPIO à l'aide d'un timer. Si notre calcul de timer est bon et la fréquence CPU correcte, alors nous devrions voir la bonne fréquene à l'oscilloscope.

NMSIS et le SysTick

Nous l'avons vu, le coeur Nuclei nous offre un timer "bonus" en plus des Timers 0 à 6 que le fondeur GigaDevices propose. Ces derniers sont assez complexes et servent généralement à sortir des PWM, compter des impulsions ou commander des moteurs. Si on peut éviter de s'en servir, profitons-en.

Comment y accéder ? Eh bien, c'est un peu obscure. En fait, la société Nuclei a pondu un ensemble de "standards" d'appellations exactement comme … ARM, avec son CMSIS et Nuclei l'a donc fort logiquement appelé NMSIS.

On y trouve donc dedans ce qui a trait au coeur, dont notre fameux SysTick.

Malheureusement, au moment de l'écriture de cet article, la librairie NMSIS s'intègre très mal à PlatformIO. Il faut donc mieux commencer par l'ensemble cohérent fournit par Nuclei, sur le Github : https://github.com/Nuclei-Software/nuclei-sdk qui contient également la librairie du GD32 adaptée pour l'occasion.

Ce qu'il est possible de faire pour le moment est de copier coller le code se rapportant au SysTick en provenance du NMSIS. Il suffit alors de récupérer l'exemple fournit par Nuclei :

static volatile uint32_t msTicks = 0;
static volatile bool tick_1s = false;
static volatile uint32_t tick_1s_counter = 0;

#define CONFIG_TICKS        (TIMER_FREQ / 1000)
#define SysTick_Handler     eclic_mtip_handler

void SysTick_Handler(void)
{                                /* SysTick interrupt Handler. */
    SysTick_Reload(CONFIG_TICKS);                            /* Call SysTick_Reload to reload timer. */
    msTicks++;                                                /* See startup file startup_gd32vf103.S for SysTick vector */
    tick_1s_counter++;
    if (tick_1s_counter >= 1000)
    {
        tick_1s_counter = 0;
        tick_1s = true;
    }
}

Et le main :

int main(void)
{
    longan_led_init();
    init_uart0();
    longan_bp1_init();

    uint32_t returnCode = SysTick_Config(CONFIG_TICKS);

    while(1)
    {
        if (tick_1s)
        {
            tick_1s = false;
            printf("[OST] SysTick=%d\r\n", (int)msTicks);
        }

    }
}

Et voilà, sur l'UART vous devriez observer le tick qui s'incrémente. N'oubliez pas le mot clé 'volatile' pour les variables incrémentées dans l'interruption : sans cela, le compilateur effectuera une optimisation ce qui rendra le code non fonctionnel ; en effet, selon son analyse la fonction SysTick_Handler n'est jamais appelée donc il va la supprimer. En ajoutant le mot clé volatile, on lui dit simplement "t'inquiète, cette variable sert bien quelque part, t'occupe et ne touche à rien".

La table des vecteurs d'interruptions est localisée dans le fichier start.S :

image

Solutions de débogage

Le coeur RISC-V étant assez nouveau, il existe peu de solutions. Je suggère notamment :

  • L'incontournable JLink, attention toutefois à la version matérielle de votre sonde ; un tableau résume quelle version est supportée : https://wiki.segger.com/Software_and_Hardware_Features_Overview
  • Sipeed USB-JTAG/TTL RISC-V Debugger, un espèce de clone de ST-Link mais qui a l'air de supporter le RISC-V, je l'ai commandé je testerai
  • BlackMagic Probe : l'intégration du RISC-V est en cours, non encore fonctionnelle a priori
  • OpenOCD via un moniteur série : je ne l'ai pas testé !

Conclusion

Voilà une carte bien sympatique. Il est maintenant temps de réaliser un petit projet avec, et si possible doté d'une sone JTAG !

Le code source du tutorial est ici : https://github.com/arabine/risc-v-tutorial

  • # Corrections

    Posté par  (site web personnel) . Évalué à 1.

    Bon ça commence bien, le titre n'est pas bon :

    "Prise en main de la carte Longan Nano RISC-V de Sipeed"

    GigaDevice étant le fondeur de la puce GD32VF103 uniquement, Sipeed a conçu la carte.

    • [^] # Re: Corrections

      Posté par  (site web personnel) . Évalué à 2.

      Superbe dépêche. Avec un bel effort de vulgarisation. Même si ça reste un peu trop pointu pour certains. Du coup, ma contribution se bornera à relever trois petites coquilles :

      « Cela permet d'offire une séparation […] »

      « Chaque coeur RISC-V dispose de non propre […] »

      « IRAFURORBREVISESTANIMUMREGEQUINISIPARETIMPERAT » — Odes — Horace

      • [^] # Re: Corrections

        Posté par  (Mastodon) . Évalué à 2.

        Corrigé, merci.

        En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

    • [^] # Re: Corrections

      Posté par  (Mastodon) . Évalué à 2. Dernière modification le 23 décembre 2020 à 18:48.

      Corrigé, merci.

      En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

    • [^] # Re: Corrections

      Posté par  (site web personnel, Mastodon) . Évalué à 2.

      Aussi

      Il est maintenant temps de réaliser un petit projet avec, et si possible doté d'une sonde JTAG !

      au lieu de

      Il est maintenant tant de réaliser un petit projet avec, et si possible doté d'une sone JTAG !

      “It is seldom that liberty of any kind is lost all at once.” ― David Hume

      • [^] # Re: Corrections

        Posté par  (Mastodon) . Évalué à 2.

        Corrigé, merci.

        En théorie, la théorie et la pratique c'est pareil. En pratique c'est pas vrai.

    • [^] # Re: Corrections

      Posté par  (site web personnel) . Évalué à 1. Dernière modification le 26 décembre 2020 à 23:30.

      mes 2 cents…

      librairie du GD32 adaptée pour l’occasion

      au lieu de

      librairie du GD32 adaptée pour l'occation

      (orthographe + apostrophe typographique française)

      Ce message ne contient aucun degré. (sérieusement…! non, mais en vrai !! ok, peut-être un peu parfois mais pas là ☻ )

  • # "accès privilégié" et "root"?

    Posté par  . Évalué à 3.

    Je n'y connais pas grand chose, mais est-ce que quand on parle de "accès privilégié" au niveau de l'ISA et Linux, on ne parle pas plutôt de la différence entre le kernel et les modules d'un côté et les process "utilisateurs" (systemd ou init et tous leurs enfants) de l'autre?
    "root" étant privilégié par le système d'exploitation (gestion de fichiers, process, …) mais pas par le CPU lui-même.

  • # DirtyJTAG

    Posté par  (site web personnel) . Évalué à 2.

    "Il est maintenant tant de réaliser un petit projet avec, et si possible doté d'une sone JTAG !"

    J'ai la même carte, je vais essayer de porter le code de DirtyJTAG dessus:

    https://github.com/jeanthom/DirtyJTAG

  • # EOL bug

    Posté par  (site web personnel) . Évalué à 2.

    Je note sur ta photo d'écran de cutecom que tu sembles avoir le même problème que moi: les \n ne passent pas. Ils sont bien dans ton code, mais le terminal ne passe pas à la ligne.

    De mon coté, je n'y arrive pas, même en tripotant les paramètres des différents terminaux que j'ai essayés (minicom, screen, platformio/python).

    Bref, quelqu'un a une idée ? J'ai l'impression qu'ils ne passent même pas "on the wire".

  • # La vérité sur le SysTick

    Posté par  (site web personnel, Mastodon) . Évalué à 2.

    Merci pour cette belle introduction à la longan nano. Juste pour préciser, avec 5€ (en fait $4.90) on a aussi l'écran (RGB), c'est pas en option !
    Et l'usb est dans le microcontrôleur contrairement à beaucoup de kit de ce type, il n'y a pas de «ftdi» à côté pour faire de la conversion usb-uart ou usb-gpio.

    L'histoire du SysTick est visiblement une histoire de copier/coller de ARM. En fait le GD32VF103CBT6 de Gigadevice est un copier-coller de leur microcontrôleur GD32E103CBT6 dont ils ont juste changé le cœur pour mettre un processeur de type Risc-V (Bumblebee).

    Et ils ont fait pareil pour leurs base de code !

    Tout le code fourni initialement en tant que librairie était en fait le code de leur microcontrôleur ARM. À la lecture de l'article il semble que ce code se soit amélioré, mais à l'époque faire fonctionner l'USB (pour en faire un périphérique afficheur de stats pour mon pc) n'était pas une mince affaire.

    À l'origine, le SysTick est du pur standard ARM, qui ne peut être présent dans un processeur Risc-V. Comme la doc était du copié collé, c'est resté (du moins au début).

    En fait, le standard Risc-V décrit des registres mstatus, mtimecmp, … qui sont accessibles via le CSR (Control Status Register). On ne peut pas lire/écrire ces registres via les instruction classiques load/store. Il faut utiliser les instructions spécifiques riscv csrrx.
    Il est donc impossible que le code ARM soit le même que le code RISC-V puisque se sont des instructions assembleur différentes, et il à donc fallu retoucher leur base de code.

    J'ai plus qu'une balle

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.