Le dossier CoCiNum/src/c
contient des fichiers sources en langage C
que vous pouvez incorporer à 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 |
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
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;
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;
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;
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 N
iè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),
...
};
// 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;
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;
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;
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 fichier MyApp.c
contiendra au moins la fonction main
et la fonction de traitement d’interruptions irq_handler
.
Le pilote de joystick fournit les fonctions suivantes (voir le fichier SPI/Joystick.h
) :
Joystick_init
: initialiser le périphérique SPI et le pilote de joystick.Joystick_update
: lire l’état du joystick.#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 ++;
}
}
}
Le pilote d’accéléromètre fournit les fonctions suivantes (voir le fichier SPI/Accelerometer.h
) :
Accelerometer_init
: initialiser le périphérique SPI et le pilote d’accéléromètre.Accelerometer_update
: lire l’état de l’accéléromètre.#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 ++;
}
}
}
Le pilote d’écran OLED fournit les fonctions suivantes (voir le fichier SPI/OLED.h
) :
OLED_init
: initialiser le périphérique SPI et le pilote d’écran OLED.OLED_clear_all
: effacer l’écran.OLED_clear
: effacer une région rectangulaire de l’écran.OLED_draw_pixel
: dessiner un pixel.OLED_draw_line
: dessiner un segment de droite.OLED_draw_rect
: dessiner le contour d’un rectangle.OLED_fill_rect
: dessiner un rectangle plein.OLED_copy
: copier une région rectangulaire vers un autre endroit de l’écran.OLED_set_bitmap
: compacter un tableau de couleurs représentant une image.OLED_draw_bitmap
: afficher une image après compactage.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 ++;
}
}
}
Le pilote de capteur de température fournit les fonctions suivantes (voir le fichier I2C/Thermometer.h
) :
Thermometer_init
: initialiser le périphérique I2C et le pilote de capteur de température.Thermometer_get_temperature
: lire la température.#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 ++;
}
}
}
Le pilote de sonar fournit les fonctions suivantes (voir le fichier UART/Sonar.h
) :
Sonar_init
: initialiser le périphérique I2C et le pilote de capteur de température.Sonar_update
: mettre à jour la distance mesurée en lisant le dernier caractère reçu sur la liaison série.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 ++;
}
}
}
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).
Les commandes suivantes compilent le programme MyApp.c
et produisent un fichier MyApp.hex
:
cd $HOME/CoCiNum/src/c/MyApp
make
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 Navigator → Program and Debug → Open Hardware Manager → Open Target → Auto-connect.
Configurez le FPGA : Flow Navigator → Program and Debug → Open Hardware Manager → Program 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.