Activité : développement logiciel embarqué

Le dossier CoCiNum/src/c contient des fichiers sources en langage C que vous pouvez incorporer à une application.

Structure d’une application

Pour créer une application MyApp, suivez les étapes suivantes.

Créez un dossier pour votre application :

cd $HOME/CoCiNum/src/c
mkdir MyApp
cd MyApp

Dans le dossier CoCiNum/src/c/MyApp, créez les fichiers suivants :

Fichier Rôle
MyApp.c Le programme principal
Platform.h La déclaration des constantes et variables représentant les périphériques
Platform.c Les données de configuration des périphériques
Makefile Un script de construction du programme exécutable

Le fichier Platform.h

Pour un système équipé d’une interface de communication série et d’un timer, le fichier Platform.h aura le contenu suivant :

#ifndef PLATFORM_H_
#define PLATFORM_H_

#include <InterruptController/InterruptController.h>
#include <UART/UART.h>
#include <Timer/Timer.h>

#define CLK_FREQUENCY_HZ     100000000

extern InterruptController *const intc;
extern UART                *const uart;
extern Timer               *const timer;

#endif

Ajout d’un périphérique SPI

Pour chaque périphérique SPI, on ajoutera des définitions de la forme :

#include <SPI/SPI.h>

extern Timer     *const spi_timer;
extern SPIMaster *const spi_master;

Si votre application utilise un module joystick sur bus SPI, vous pouvez ajouter :

#include <SPI/Joystick.h>

extern SPIDevice *const jstk;

Si votre application utilise un module accéléromètre sur bus SPI, vous pouvez ajouter :

#include <SPI/Accelerometer.h>

extern SPIDevice *const acl;

Si votre application utilise un écran OLED sur bus SPI, vous pouvez ajouter :

#include <SPI/OLED.h>

extern SPIDevice *const spi_dev;
extern OLED *const oled;

Ajout d’un périphérique I2C

Pour un périphérique I2C, on ajoutera des définitions de la forme :

#include <I2C/I2C.h>

extern I2CMaster *const i2c;

Si votre application utilise un module capteur de température I2C, vous pouvez ajouter :

#include <I2C/Thermometer.h>

extern I2CDevice *const tmp;

Ajout d’un périphérique UART

Pour un périphérique récepteur série, on ajoutera des définitions de la forme :

extern UART *const sonar_uart;

Si votre application utilise un module sonar, vous pouvez ajouter :

#include <UART/Sonar.h>

extern Sonar *const sonar;

Le fichier Platform.c

Ce fichier contient des instances des structures qui représentent les périphériques du système. La macro EVT_MASK convertit un numéro d’événement en un masque : pour un numéro d’événement N, elle retourne un entier dont le Nième bit vaut 1.

#include "Platform.h"

// Le contrôleur d'interruptions.
InterruptController *const intc = (InterruptController*)0x81000000;

// L'interface série asynchrone (UART).
static UART uart_priv = {
    // L'adresse de base des registres de l'UART.
    .address     = 0x82000000,
    // Le masque des événements en réception.
    .rx_evt_mask = EVT_MASK(0),
    // Le masque des événements en émission.
    .tx_evt_mask = EVT_MASK(1),
    // Le contrôleur d'interruption qui gère les événements de l'UART
    .intc        = intc
};

UART *const uart = &uart_priv;

// Le timer à usage général.
static Timer timer_priv = {
    // L'adresse de base des registres du timer.
    .address  = 0x83000000,
    // Le masque des événements périodiques.
    .evt_mask = EVT_MASK(2),
    // Le contrôleur d'interruption qui gère les événements du timer
    .intc     = intc
};

Timer *const timer = &timer_priv;

Les adresses et les numéros d’événemets renseignés dans Platform.c doivent être cohérents avec ceux définis dans Computer_pkg.vhd.

-- Computer_pkg.vhd
constant TIMER_ADDRESS : byte_t := x"83";

constant INTC_EVENTS_TIMER : natural := 2;
// Platform.c
static Timer timer_priv = {
    .address  = 0x83000000,
    .evt_mask = EVT_MASK(2),
    ...
};

Définition des périphériques SPI

// Le timer utilisé pour les communications SPI.
static Timer spi_timer_priv = {
    .address  = 0x84000000,
    .evt_mask = EVT_MASK(3),
    .intc     = intc
};

Timer *const spi_timer = &spi_timer_priv;

// Le contrôleur SPI.
static SPIMaster spi_master_priv = {
    // L'adresse de base des registres du contrôleur SPI.
    .address  = 0x85000000,
    // Le masque des événements de fin de trame.
    .evt_mask = EVT_MASK(4),
    // Le contrôleur d'interruption qui gère les événements du contrôleur SPI.
    .intc     = intc
};

SPIMaster *const spi_master = &spi_master_priv;

Si votre application utilise un module joystick sur bus SPI, vous pouvez ajouter :

static SPIDevice jstk_priv = {
    // Le contrôleur SPI utilisé pour communiquer avec le joystick.
    .spi            = spi_master,
    // Le timer utilisé pour mesurer les temps d'attente.
    .timer          = spi_timer,
    // La polarité de l'horloge SPI.
    .polarity       = 0,
    // La phase de l'horloge SPI.
    .phase          = 0,
    // La vitesse de communication, en périodes d'horloge par bit.
    .cycles_per_bit = CLK_FREQUENCY_HZ / 1000000, // 1 Mbit/sec
    // Le temps d'attente, en périodes d'horloge.
    .cycles_per_gap = CLK_FREQUENCY_HZ / 25000    // 40us (> 25 us)
};

SPIDevice *const jstk = &jstk_priv;

Si votre application utilise un module accéléromètre sur bus SPI, vous pouvez ajouter :

static SPIDevice acl_priv = {
    // Le contrôleur SPI utilisé pour communiquer avec l'accéléromètre.
    .spi            = spi_master,
    // Le timer utilisé pour mesurer les temps d'attente.
    .timer          = spi_timer,
    // La polarité de l'horloge SPI.
    .polarity       = 0,
    // La phase de l'horloge SPI.
    .phase          = 0,
    // La vitesse de communication, en périodes d'horloge par bit.
    .cycles_per_bit = CLK_FREQUENCY_HZ / 2000000, // 2 Mbit/sec
    // Le temps d'attente, en périodes d'horloge.
    .cycles_per_gap = CLK_FREQUENCY_HZ / 5000000  // 200ns (> 100 ns)
};

SPIDevice *const acl = &acl_priv;

Si votre application utilise un écran OLED sur bus SPI, vous pouvez ajouter :

static SPIDevice spi_dev_priv = {
    .spi            = spi_master,
    .timer          = spi_timer,
    .polarity       = 1,
    .phase          = 1,
    .cycles_per_bit = CLK_FREQUENCY_HZ / 5000000, // 5 Mbit/sec
    .cycles_per_gap = CLK_FREQUENCY_HZ / 5000000  // 200 ns
};

SPIDevice *const spi_dev = &spi_dev_priv;

static OLED oled_priv = {
    .spi        = spi_dev,
    .address    = 0x80000002,
    .cycles_1ms = CLK_FREQUENCY_HZ / 1000
};

OLED *const oled = &oled_priv;

Définition des périphériques I2C

static I2CMaster i2c_priv = {
    // L'adresse de base des registres du contrôleur I2C.
    .address  = 0x84000000,
    // Le masque des événements de fin de trame.
    .evt_mask = EVT_MASK(3),
    // Le contrôleur d'interruption qui gère les événements du contrôleur I2C.
    .intc     = intc
};

I2CMaster *const i2c = &i2c_priv;

Si votre application utilise un module capteur de température sur bus I2C, vous pouvez ajouter :

static I2CDevice tmp_priv = {
    // Le contrôleur I2C utilisé pour communiquer avec le capteur de température.
    .i2c           = i2c,
    // L'adresse du capteur de température sur le bus I2C.
    .slave_address = 0x4B
};

I2CDevice *const tmp = &tmp_priv;

Définition des périphériques UART supplémentaires

Pour un récepteur série destiné à communiquer avec un sonar, ajoutez :

static UART sonar_uart_priv = {
    .address     = 0x84000000,
    .rx_evt_mask = EVT_MASK(3),
    .tx_evt_mask = EVT_MASK_OFF, // Pas d'événement en émission sur cette interface.
    .intc        = intc
};

UART *const sonar_uart = &sonar_uart_priv;

Pour le module sonar proprement dit, ajoutez :

static Sonar sonar_priv = {
    .uart = sonar_uart
};

Sonar *const sonar = &sonar_priv;

Le fichier Makefile

Ce fichier décrit la séquence de compilation de l’application. Il s’appuie sur le fichier Virgule.make présent dans le dossier CoCiNum/scripts.

# La taille de la RAM, en octets.
MEM_SIZE = 65536

# La liste des fichiers sources à ajouter à l'application, en plus de MyApp.c
C_DEPS = \
	../LibC/string.c \
    ../Utilities/int_to_string.c \
	../InterruptController/InterruptController.c \
	../UART/UART-buffered.c \
    ../UART/Sonar.c \
	../Timer/Timer.c \
	../SPI/SPI.c \
	../SPI/Joystick.c \
	../SPI/Accelerometer.c \
    ../SPI/OLED.c \
    ../I2C/I2C.c \
    ../I2C/Thermometer.c \
	Platform.c

# Les options de compilation de cette application.
# Ici, minimiser la taille du programme exécutable.
C_FLAGS_USER = -Os

# Ce Makefile compilera le programme MyApp.c
# et produira un exécutable au format Intel hex.
all: MyApp.hex

include ../../../scripts/Virgule.make

Le programme principal

Le fichier MyApp.c contiendra au moins la fonction main et la fonction de traitement d’interruptions irq_handler.

Squelette d’application utilisant un joystick sur bus SPI

Le pilote de joystick fournit les fonctions suivantes (voir le fichier SPI/Joystick.h) :

#include "Platform.h"

static volatile unsigned tick;

__attribute__((interrupt("machine")))
void irq_handler(void) {
    // Appeler le gestionnaire d'interruptions du pilote de l'interface série.
    UART_irq_handler(uart);

    // Incrémenter le compteur tick à chaque interruption du timer.
    if (Timer_has_events(timer)) {
        Timer_clear_event(timer);
        tick ++;
    }
}

void main(void) {
    // Initialiser le pilote de l'interface série
    // et afficher un message de bienvenue.
    UART_init(uart);
    UART_puts(uart, "Joystick Demo.\n");

    // Configurer le timer pour demander des interruptions
    // dix fois par seconde.
    Timer_init(timer);
    Timer_set_limit(timer, CLK_FREQUENCY_HZ / 10);
    Timer_enable_interrupts(timer);

    // Initialiser le contrôleur SPI et le pilote du joystick.
    Joystick_init(jstk);

    JoystickState jstk_state;

    tick = 0;
    unsigned tock = 0;

    // Exécuter jusqu'à ce que l'utilisateur presse une touche.
    while (!UART_has_data(uart)) {
        // Si une ou plusieurs interruptions du timer ont été détectées.
        if (tick != tock) {
            // Configurer la couleur de la LED du joystick.
            jstk_state.red   = ...;
            jstk_state.green = ...;
            jstk_state.blue  = ...;

            // Mettre à jour la couleur de la LED du joystick,
            // lire les coordonnées du joystick et l'état des boutons.
            Joystick_update(jstk, &jstk_state);

            // Ici, vous pouvez utiliser les champs suivants :
            // jstk_state.x       : la coordonnée X du joystick (0 à 1023)
            // jstk_state.y       : la coordonnée Y du joystick (0 à 1023)
            // jstk_state.trigger : vaut 1 si l'utilisateur presse la gachette
            // jstk_state.pressed : vaut 1 si l'utilisateur presse la manette

            tock ++;
        }
    }
}

Squelette d’application utilisant un accéléromètre sur bus SPI

Le pilote d’accéléromètre fournit les fonctions suivantes (voir le fichier SPI/Accelerometer.h) :

#include "Platform.h"

static volatile unsigned tick;

__attribute__((interrupt("machine")))
void irq_handler(void) {
    // Appeler le gestionnaire d'interruptions du pilote de l'interface série.
    UART_irq_handler(uart);

    // Incrémenter le compteur tick à chaque interruption du timer.
    if (Timer_has_events(timer)) {
        Timer_clear_event(timer);
        tick ++;
    }
}

void main(void) {
    // Initialiser le pilote de l'interface série
    // et afficher un message de bienvenue.
    UART_init(uart);
    UART_puts(uart, "Accelerometer Demo.\n");

    // Configurer le timer pour demander des interruptions
    // 10 fois par seconde.
    Timer_init(timer);
    Timer_set_limit(timer, CLK_FREQUENCY_HZ / 10);
    Timer_enable_interrupts(timer);

    // Initialiser le contrôleur SPI et le pilote de l'accéléromètre.
    Accelerometer_init(acl);

    AccelerometerState acl_state;

    tick = 0;
    unsigned tock = 0;

    // Exécuter jusqu'à ce que l'utilisateur presse une touche.
    while (!UART_has_data(uart)) {
        // Si une ou plusieurs interruptions du timer ont été détectées.
        if (tick != tock) {
            // Interroger l'accéléromètre.
            Accelerometer_update(acl, &acl_state);

            // Ici, vous pouvez utiliser les champs suivants :
            // acl_state.x : l'accélération selon l'axe X
            // acl_state.y : l'accélération selon l'axe Y
            // acl_state.z : l'accélération selon l'axe Z
            // acl_state.t : la température mesurée par l'accéléromètre

            tock ++;
        }
    }
}

Squelette d’application utilisant un écran OLED sur bus SPI

Le pilote d’écran OLED fournit les fonctions suivantes (voir le fichier SPI/OLED.h) :

L’exemple ci-dessous

#include "Platform.h"

static volatile unsigned tick;

__attribute__((interrupt("machine")))
void irq_handler(void) {
    // Appeler le gestionnaire d'interruptions du pilote de l'interface série.
    UART_irq_handler(uart);

    // Incrémenter le compteur tick à chaque interruption du timer.
    if (Timer_has_events(timer)) {
        Timer_clear_event(timer);
        tick ++;
    }
}

// Définir des couleurs {Rouge, Vert, Bleu}.
#define B {0,  0,  0}
#define C {31, 31, 0}
#define W {31, 31, 31}

// Définir la taille de l'objet à afficher.
#define SPRITE_WIDTH  7
#define SPRITE_HEIGHT 7
#define SPRITE_SIZE_PIX   (SPRITE_WIDTH * SPRITE_HEIGHT)
#define SPRITE_SIZE_BYTES (SPRITE_SIZE_PIX * 2)

// L'image, représentée par un tableau de couleurs.
static const OLEDColor sprite[SPRITE_SIZE_PIX] = {
    B, B, W, W, W, B, B,
    B, C, W, W, W, W, B,
    C, C, W, W, W, W, W,
    C, C, C, W, W, W, W,
    C, C, C, C, W, W, W,
    B, C, C, C, C, C, B,
    B, B, C, C, C, B, B
};

// Tableau qui recevra l'image compactée.
static uint8_t bitmap[SPRITE_SIZE_BYTES];

void main(void) {
    // Initialiser le pilote de l'interface série
    // et afficher un message de bienvenue.
    UART_init(uart);
    UART_puts(uart, "OLED Display Demo.\n");

    // Configurer le timer pour demander des interruptions
    // 20 fois par seconde.
    Timer_init(timer);
    Timer_set_limit(timer, CLK_FREQUENCY_HZ / 20);
    Timer_enable_interrupts(timer);

    // Initialiser le contrôleur SPI et le pilote de l'afficheur.
    OLED_init(oled);

    tick = 0;
    unsigned tock = 0;

    // Définir les coordonnées initiales du sprite.
    uint8_t x1 = 0;
    uint8_t y1 = 0;
    uint8_t x2 = x1 + SPRITE_WIDTH  - 1;
    uint8_t y2 = x2 + SPRITE_HEIGHT - 1;

    // Encoder le sprite, puis l'afficher.
    OLED_set_bitmap(bitmap, SPRITE_SIZE_PIX, sprite);
    OLED_draw_bitmap(oled, x1, y1, x2, y2, SPRITE_SIZE_BYTES, bitmap);

    // Exécuter jusqu'à ce que l'utilisateur presse une touche.
    while (!UART_has_data(uart)) {
        // Si une ou plusieurs interruptions du timer ont été détectées.
        if (tick != tock) {
            // Calculer la nouvelle position du sprite.
            // Mettre à jour x1, y1, x2 et y2.

            // Déplacer le sprite.
            // Utiliser les fonctions OLED_copy et OLED_clear
            // pour copier ou effacer une région rectangulaire de l'écran.

            tock ++;
        }
    }
}

Squelette d’application utilisant un capteur de température sur bus I2C

Le pilote de capteur de température fournit les fonctions suivantes (voir le fichier I2C/Thermometer.h) :

#include "Platform.h"

static volatile unsigned tick;

__attribute__((interrupt("machine")))
void irq_handler(void) {
    // Appeler le gestionnaire d'interruptions du pilote de l'interface série.
    UART_irq_handler(uart);

    // Incrémenter le compteur tick à chaque interruption du timer.
    if (Timer_has_events(timer)) {
        Timer_clear_event(timer);
        tick ++;
    }
}

void main(void) {
    // Initialiser le pilote de l'interface série
    // et afficher un message de bienvenue.
    UART_init(uart);
    UART_puts(uart, "Thermometer Demo.\n");

    // Configurer le timer pour demander des interruptions
    // dix fois par seconde.
    Timer_init(timer);
    Timer_set_limit(timer, CLK_FREQUENCY_HZ / 10);
    Timer_enable_interrupts(timer);

    // Initialiser le contrôleur I2C et le pilote du capteur de température.
    Thermometer_init(tmp);

    tick = 0;
    unsigned tock = 0;

    // Exécuter jusqu'à ce que l'utilisateur presse une touche.
    while (!UART_has_data(uart)) {
        // Si une ou plusieurs interruptions du timer ont été détectées.
        if (tick != tock) {
            int16_t t = Thermometer_get_temperature(tmp);

            // Ici, t contient la température courante, en 1/16 de degrés.

            tock ++;
        }
    }
}

Squelette d’application utilisant un sonar

Le pilote de sonar fournit les fonctions suivantes (voir le fichier UART/Sonar.h) :

Le sonar envoie la valeur de la distance sous la forme d’une chaîne de caractères terminée par un saut de ligne. Pour obtenir une valeur de distance, il faut appeler Sonar_update de manière répétitive jusqu’à ce que tous les caractères de la ligne aient été traités.

Dans l’exemple ci-dessous, l’utilisation des interruptions permet de mémoriser ces caractères au moment où ils sont reçus, en attendant qu’ils soient traités.

#include "Platform.h"

static volatile unsigned tick;

__attribute__((interrupt("machine")))
void irq_handler(void) {
    // Appeler le gestionnaire d'interruptions du pilote de l'interface série.
    UART_irq_handler(uart);
    UART_irq_handler(sonar_uart);

    // Incrémenter le compteur tick à chaque interruption du timer.
    if (Timer_has_events(timer)) {
        Timer_clear_event(timer);
        tick ++;
    }
}

void main(void) {
    // Initialiser le pilote de l'interface série
    // et afficher un message de bienvenue.
    UART_init(uart);
    UART_puts(uart, "Sonar Demo.\n");

    // Configurer le timer pour demander des interruptions
    // dix fois par seconde.
    Timer_init(timer);
    Timer_set_limit(timer, CLK_FREQUENCY_HZ / 10);
    Timer_enable_interrupts(timer);

    // Initialiser le récepteur série associé au sonar
    Sonar_init(sonar);

    tick = 0;
    unsigned tock = 0;

    // Exécuter jusqu'à ce que l'utilisateur presse une touche.
    while (!UART_has_data(uart)) {
        // Mettre à jour la distance mesurée par le sonar.
        uint8_t dist = Sonar_update(sonar);

        if (tick != tock) {
            // Ici, on peut faire quelque chose avec dist.
            // ...

            tock ++;
        }
    }
}

Fonctions d’affichage

La bibliothèque C fournie est très limitée. Par exemple, vous n’aurez pas accès aux fonctions printf ou malloc.

Pour affichez des messages dans un terminal série, vous pouvez utiliser les fonctions UART_putc et UART_puts de la manière suivante :

// Envoyer le caractère 'X' sur la liaison série désignée par la variable uart.
UART_putc(uart, 'X');

// Envoyer la chaîne "Hello", suivie d'un saut de ligne, sur la liaison série.
UART_puts(uart, "Hello\n");

Pour afficher des nombres, vous devez d’abord les convertir en chaînes de caractères. Des fonctions de conversion sont déclarées dans le fichier Utilities/int_to_string.h que vous devez d’abord inclure :

#include <Utilities/int_to_string.h>

La fonction uint32_to_string prend un entier non signé sur 32 bits, calcule sa représentation décimale et la range dans une chaîne de caractères. La fonction int32_to_string fonctionne de la même manière avec des entiers négatifs ou positifs. Les valeurs négatives sont alors précédées par le signe « - ».

// Déclarer un tableau de caractères capable de recevoir
// la représentation décimale d'un nombre de 32 bits.
char s[INT_TO_STRING_LEN];

// Convertir un entier en chaîne.
int32_to_string(s, 4925);

// Afficher le résultat, suivi d'un saut de ligne.
UART_puts(uart, s);
UART_putc(uart, '\n');

Comme Virgule ne possède pas d’instructions de multiplication et de division, les fonctions uint32_to_string et int32_to_string utilisent un algorithme par décalages et soustractions successives.

Cet algorithme est relativement lent. Dans le pire des cas, nous avons mesuré une durée de conversion d’environ 116 microsecondes (c’est-à-dire 11600 périodes d’horloge).

Compilation

Les commandes suivantes compilent le programme MyApp.c et produisent un fichier MyApp.hex :

cd $HOME/CoCiNum/src/c/MyApp
make

Chargement sur la cible

Avant de mettre la carte sous tension, reliez le module périphérique au connecteur d’extension de la carte Basys3. Vérifiez la correspondance des broches.

Reliez le connecteur micro-USB de la carte à un port USB de votre PC et mettez la carte sous tension. Ouvrez ensuite un terminal pour port série :

cd $HOME/CoCiNum
./scripts/gtkterm

Dans le menu Configuration de GTKTerm, activez l’option CR LF auto.

Connectez Vivado à votre carte Basys3 : Flow NavigatorProgram and DebugOpen Hardware ManagerOpen TargetAuto-connect.

Configurez le FPGA : Flow NavigatorProgram and DebugOpen Hardware ManagerProgram Device.

Comme lors de l’étape précédente, la fenêtre GTKTerm doit à présent afficher le texte :

\\// This is the Virgule program loader.
\\// Send an hex file to execute or press ESC to switch into interactive mode.

Dans la fenêtre terminal de Linux qui vous a servi à lancer GTKTerm, exécutez la commande :

cat $HOME/CoCiNum/src/c/MyApp/MyApp.hex > /dev/ttyUSB1

Observez le résultat de l’exécution du programme. Dans GTKTerm, pressez une touche pour terminer.