IMA5 2018/2019 P19 : Différence entre versions
(→Module FFT) |
(→Block desgin final) |
||
(23 révisions intermédiaires par le même utilisateur non affichées) | |||
Ligne 1 270 : | Ligne 1 270 : | ||
:https://www.xilinx.com/support/documentation/ip_documentation/axi_dma/v7_1/pg021_axi_dma.pdf | :https://www.xilinx.com/support/documentation/ip_documentation/axi_dma/v7_1/pg021_axi_dma.pdf | ||
:https://lauri.xn--vsandi-pxa.com/hdl/zynq/xilinx-dma.html | :https://lauri.xn--vsandi-pxa.com/hdl/zynq/xilinx-dma.html | ||
+ | |||
+ | '''Vivado''' | ||
On commence par créer un projet Vivado puis le block design suivant : | On commence par créer un projet Vivado puis le block design suivant : | ||
Ligne 1 278 : | Ligne 1 280 : | ||
:L'IP axis_data_fifo permet une meilleur stabilité. | :L'IP axis_data_fifo permet une meilleur stabilité. | ||
+ | Puis générer le bitstream, exporter la partie hardware, et créer le FSBL. | ||
+ | |||
+ | '''Petalinux''' | ||
Il faut ensuite créer un projet petalinux (comme dans cette [[IMA5 2018/2019 P19#Création de l'image Petalinux|partie]]). | Il faut ensuite créer un projet petalinux (comme dans cette [[IMA5 2018/2019 P19#Création de l'image Petalinux|partie]]). | ||
Ligne 1 494 : | Ligne 1 499 : | ||
On créer un projet Vivado avec le block design suivant : | On créer un projet Vivado avec le block design suivant : | ||
− | [[File: fft_block_design_1.png]] | + | [[File: fft_block_design_1.png|1300px]] |
:En entrée et en sortie de la FFT, des buffer ont été ajoutés pour plus de stabilité. | :En entrée et en sortie de la FFT, des buffer ont été ajoutés pour plus de stabilité. | ||
Ligne 1 508 : | Ligne 1 513 : | ||
Ce qui donne les bus suivant : | Ce qui donne les bus suivant : | ||
− | [[File: | + | [[File: fft_axi_stream.png|300px]] |
:'''N.B.''' SCALE_SCH est en fait inutile puisque en floating point l'option de scale est désactivée. | :'''N.B.''' SCALE_SCH est en fait inutile puisque en floating point l'option de scale est désactivée. | ||
Ligne 1 514 : | Ligne 1 519 : | ||
L'IP AXI DMA est configuré tel que : | L'IP AXI DMA est configuré tel que : | ||
− | [[File: axi_dma_config.png]] | + | [[File: axi_dma_config.png|400px]] |
:La taille des memory maps et des streams est de 64 bits pour correspondre aux bus de la FFT. | :La taille des memory maps et des streams est de 64 bits pour correspondre aux bus de la FFT. | ||
Ligne 1 520 : | Ligne 1 525 : | ||
Enfin, les IPs axis_data_fifo sont configuré de la manière suivante : | Enfin, les IPs axis_data_fifo sont configuré de la manière suivante : | ||
− | [[File: stream_fifo_config]] | + | [[File: stream_fifo_config.png|400px]] |
:Encore une fois, la taille du bus est de 64 bits. | :Encore une fois, la taille du bus est de 64 bits. | ||
Ligne 1 746 : | Ligne 1 751 : | ||
} | } | ||
} | } | ||
− | + | ||
void memdump(void* virtual_address, int byte_count) { | void memdump(void* virtual_address, int byte_count) { | ||
char *p = virtual_address; | char *p = virtual_address; | ||
Ligne 1 845 : | Ligne 1 850 : | ||
</div></div> | </div></div> | ||
− | ==== | + | :Ce programme reprend le code de axidma.c pour réaliser le transfert axi stream. Il y a en plus deux structures de 1024 x 2 x 32 bits correspondant à la donnée qui va être envoyée à la FFT et celle qui sera reçue. Les données de la sinusoïde sont donc chargées dans la structure d'entrée qui est ensuite copiée dans la plage mémoire qui est envoyée en stream par l'axi_dma à la FFT. Les données reçus de la FFT sont copiées dans la structure de sortie puis enregistrées dans un fichier. |
+ | |||
+ | ====Résultats de la FFT==== | ||
+ | |||
+ | Pour utiliser les résultats de la FFT, nous allons encore une fois créer un IP custom. Cet IP permettra d'envoyer au CPU la bande passante du signal (à -3 dB) ainsi que la puissance maximale (en dBm). | ||
'''Notes''' | '''Notes''' | ||
:Pour déboguer le code de la partie FPGA, il est intéressant d'avoir un simulateur VHDL / verilog afin de ne pas avoir à recompiler le bitstream et reprogrammer le FPGA à chaque fois. | :Pour déboguer le code de la partie FPGA, il est intéressant d'avoir un simulateur VHDL / verilog afin de ne pas avoir à recompiler le bitstream et reprogrammer le FPGA à chaque fois. | ||
:Pour déboguer le code implémenté sur le matériel, il est mieux d'allouer plus de registres que nécessaire afin de stocker des valeurs intermédiaires et les vérifier. | :Pour déboguer le code implémenté sur le matériel, il est mieux d'allouer plus de registres que nécessaire afin de stocker des valeurs intermédiaires et les vérifier. | ||
+ | |||
+ | =====Modification du block design===== | ||
+ | |||
+ | Maintenant, les résultats de la FFT ne sont plus envoyés vers le CPU mais seront envoyés vers un IP custom. Il faut donc supprimer le canal AXIS-S2MM de l'IP DMA. Le block design devient alors comme suit. | ||
+ | |||
+ | [[File: fft_block_desgin_2.png | 1000px]] | ||
+ | |||
+ | =====Calcul de la puissance===== | ||
+ | |||
+ | Les résultats étant en flottants, j'ai utilisé des IPs permettant de réaliser des calculs sur des flottant puis de les convertir en entiers pour les exploiter dans la logique. Le sous diagramme power_calc réalise le calcul de la puissance en suivant l'équation : P(dB) = 1000log((re² + im²) / points²). J'ai ajouter un facteur 100 pour garder la précision lors de la conversion en entier. | ||
+ | |||
+ | Le block design est le suivant : | ||
+ | |||
+ | [[File: power_calc.png|1000px]] | ||
+ | |||
+ | Le stream est d'abord divisé en deux pour séparer la partie réelle et la partie imaginaire qui sont ensuite élevées au carré puis additionnées. Cet addition est alors divisée par le nombre de point au carré puis passé dans un logarithme népérien avant d'être converti en logarithme à base 10 et multiplié par 1000. Enfin la conversion en entier est faite. | ||
+ | |||
+ | On remarque les connections des différents t_valid pour synchroniser les calculs. | ||
+ | |||
+ | =====IP Custom===== | ||
+ | |||
+ | L'IP utilisé pour analysé les résultats dispose d'un bus d'entrée de 32 bits pour recevoir la puissance et d'une interface AXI slave pour que le CPU puisse récupérer les résultats. Il envoie d'ailleurs une interruption au CPU lorsque les résultats sont prêts. | ||
+ | |||
+ | L'IP dispose aussi d'un système de reset envoyé depuis le CPU qui permet de refaire un calcul. | ||
+ | |||
+ | Lorsque le bus de résultats arrive, les valeurs de puissance sont stockées dans un tableau (une comparaison simple est faite pour trouver l'indexe de puissance maximale). En parallèle la fréquence associée à la puissance est calculée avec l'équation F = index * Fe / N (où Fe est la fréquence d’échantillonnage et N le nombre de points). | ||
+ | |||
+ | Une fois que tous les résultats sont obtenus, la logique part de l'indexe de puissance pour effectuer une comparaison sur les indexes supérieurs et inférieurs dans le but de trouver la bande passante à -3 db. | ||
+ | |||
+ | Enfin l'interruption est envoyée et les fréquences (haute et basse) de la bande passante sont stockées dans des registres, ainsi que la puissance maximale. | ||
+ | |||
+ | ===Module 3=== | ||
+ | |||
+ | Ce module FPGA est constitué de 3 IPs : | ||
+ | * Un générateur de trafic TCP/IP UDP/IP utilisé pour les tests | ||
+ | * Un parser permettant de récupérer des informations dans les paquets | ||
+ | * Un compteur qui vérifie le nombre de paquets reçus (adresse et port de destination) afin d'envoyer des avertissements. | ||
+ | |||
+ | ====IP gérant du traffic TCP/IP ou UDP/IP==== | ||
+ | |||
+ | Cet IP génère du trafic ethernet IPv4 TCP ou UDP utilisé pour les tests. Pour générer le trafic j’utilise une sorte de machine à état avec un curseur qui boucle sur les différents headers ethernet, IP, TCP, ou UDP. Le trafic est synchroniser entre le générateur et le receveur grâce au préambule du header ethernet, qui doit normalement être utilisé pour synchroniser les horloges. La plupart des headers ont une valeur par défaut, seulement les adresses IP, les ports et le protocol peuvent être choisi depuis le CPU grâce à une application C qui permet de choisir le type de trafic, par exemple : | ||
+ | |||
+ | $ ip-order -x -p TCP -s 192.168.10.51:22 -d 192.168.10.50:22 | ||
+ | Permet de générer du trafic TCP, tandis que : | ||
+ | |||
+ | $ ip-order -k | ||
+ | Stop le trafic. | ||
+ | |||
+ | Le générateur est tout de même capable de préciser la taille des paquets avec les headers IPv4 internet header length (qui définit la taille du header IPv4) et total length (qui définit la taille du paquet IPv4), ainsi que le header TCP data offset (qui définit la taille du header TCP) ou le header UDP length (qui définit la taille du paquet UDP, sachant que le header fait toujours 8 octets). | ||
+ | |||
+ | ====IP analysant les trames==== | ||
+ | |||
+ | Le parser reçoit le trafic et l’analyse. Il enregistre notamment les adresses IP, les ports et le protocol, qui sont ensuite envoyés au compteur pour contrôler le nombres de requêtes. En parallèle, il transfère les paquets à un destinataire. Comme avec le premier IP, j’utilise un curseur pour parcourir les headers. La taille des paquets est obtenus grâces aux headers pour ne pas faire d’erreur de parsing. Durant chaque transfert de trame, dès lors que l’IP connaît l'adresse IP de destination, il vérifie si cette adresse est désignée (par le troisième IP) comme subissant une attaque ; si c’est le cas, le transfert du paquet est stoppé et le curseur de la machine à état retourne à l’état initial. Il en est de même lorsque le port de destination est connu. | ||
+ | |||
+ | ====IP émettant les alertes==== | ||
+ | |||
+ | Le compteur analyse les informations reçues par le parser et bloque le trafic en fonction de ces informations et des ordres du CPU. La vérification se fait sur une fenêtre de dix secondes grâce à un compteur. Chaque adresse IP et port protégé dispose de son propre compteur. A chaque fois qu’un paquet a été analysé, le compteur reçoit l’adresse et le port de destination du paquet de la part du parser. Il vérifie alors si l’un des deux est inclu dans ceux qu’il protège, si c’est le cas le compteur associé est incrémenté. Lorsqu’un compteur associé à une adresse ou un port protégé dépasse le seuil fixé par le CPU, une interruption est envoyé et le parser est informé. A l’expiration de la fenêtre de dix secondes, tous les compteurs sont réinitialisés. | ||
+ | Pour configurer le compteur, j’utilise une application C qui permet de définir le seuil (en paquets par seconde sur une fenêtre de dix secondes) à partir duquel une adresse ou un port est bloqué, par exemple : | ||
+ | |||
+ | $ ip-config -t 10000 | ||
+ | Fixe le seuil à 10 000 paquets par seconde. | ||
+ | |||
+ | La même application est utilisée pour configurer les adresses ou ports protégés : | ||
+ | |||
+ | $ ip-config -i 0 -a 192.168.10.50 | ||
+ | $ ip-config -i 1 -p 22 | ||
+ | Configure respectivement l’adresse 192.168.10.50 en tant que première adresse protégée et le port 22 en tant que deuxième port protégé. | ||
+ | |||
+ | Enfin, une dernière application permet d’intercepter les interruptions du compteur et d’afficher l’adresse ou le port bloqué dans le terminal. | ||
+ | |||
+ | ====Block desgin final==== | ||
+ | |||
+ | [[File: ip_block_design.png]] | ||
+ | |||
+ | On remarque (en vert) le bus permettant de faire circuler le trafic entre le générateur et le parser. Et en rouge, l'interruption envoyé par le compteur au CPU. Les liens entre le parser et le compteur permettent d'échanger des informations pour bloquer les adresses ip ou les ports. | ||
=Documents Rendus= | =Documents Rendus= | ||
+ | [[File: Rapport_PFE_P19_MACHEREZ.pdf]] |
Version actuelle datée du 1 mars 2019 à 10:29
Sommaire
Détection de menaces IOT sur FPGA
Présentation générale
De nos jours, l'informatique s'oriente vers une philosophie microservices. C'est-à-dire que l'on conçoit une multitude de tâches simples, qui permettent de réaliser un système plus complexe une fois rassemblées. L'IOT (internet des objets) est conçu sur ce principe. Par exemple, dans le cadre de la domotique, une maison est "connectée" grâce à une base sur laquelle viennent s'appairer plusieurs objets (capteurs, actionneurs, indicateurs, ...), constituant alors un réseau complet. Le problème est que par soucis de praticité, la grande majorité des objets IOT fonctionnent sur batterie, ce sont donc de très petit système incapables de se défendre en cas d'attaques.
Description
Ce projet a pour but de proposer un système permettant de détecter de possibles menaces sur un réseau IOT en temps réel.
Objectifs
- Lister les attaques possibles sur un réseau IOT
- Mettre en place un réseau IOT permettant d'étudier certaines attaques
- Récupérer les différentes informations des objets connectés sur un FPGA
- Agréger ces données pour en sortir des modèles
- A partir de ces modèles, être capable de détecter de possibles attaques
- Proposer un système de dashboard / alerting permettant de visualiser l'état du réseau
Cahier des charges
Le système sera composé d'une base (sur laquelle les objets seront connectés) constituée d'un FPGA couplé à un processeur fonctionnant avec linux. Ce système pourra détecter des menaces réseaux (déni de service, homme du milieu, ...) ou des menaces physiques (brouillage, diminution de la batterie, ...).
- Il faudra dans un premier temps recenser les différentes attaques possibles sur un réseau IOT puis déterminer celles qui pourront être détectées (par exemple le eavesdropping - consistant à juste écouter le traffic - ne sera pas détectable).
- Ensuite, un réseau IOT simple sera mis en place, ce dernier sera constitué de la base (décrite ci-dessus) et de quelques nœuds communicant avec cette base. Deux cas seront à prendre en compte, le premier est une communication direct entre un nœud et la base, le second est une communication d'un nœud vers un autre noeud qui relayera vers la base. Le protocole de communication sera choisi parmi ceux qui sont les plus utilisés en IOT (BLE, Z-Wave et ZigBee pour les courtes distances ; Lora et SigFox pour les distances plus longues).
- Une fois le réseau mis en place, il faudra récolter certaines informations concernant les nœuds et les agréger sur la base dans le but de définir les comportements normaux des nœuds.
- Après avoir définit ces comportements normaux, les attaques retenues dans le point 2 seront testées sur le réseau afin de voir leur(s) impacte(s) sur les données récoltées et d'en déduire des seuils permettant de basculer entre l'état normal et l'état anormal.
- Enfin, un système de dashboard sera proposé pour visualiser l'état du réseau IOT et émettre des alertes.
Réalisation du Projet
Liste non exhaustive des attaques possibles sur un réseau IOT
Couche Applicative
La couche applicative permet d'améliorer l'expérience utilisateur, elle induit aussi de possibles attaques :
- Injection de code par l'exploitation de bug : le plus souvent réaliser avec un "buffer overflow" qui va corrompre la mémoire vive et permettre d'injecter du code exécutable.
- Autorisation : beaucoup d'objets IOT ont la configuration par défaut (par exemple admin:admin comme logins) ce qui permet d'avoir un accès direct au shell de l'objet en question.
Couche Réseau
La couche réseau constitue les différents protocoles et trames utilisés par les objets pour communiquer, c'est la couche ou l'on retrouve le plus d'attaques possibles :
- Déni de service (DOS) : envoyer un nombre important de requêtes pour submerger le trafic.
- Homme du milieu (MITM) : intercepter le trafic venant d'un objet puis le transmettre à la base (ou inversement), permettant d'écouter ou de modifier le trafic de manière incognito.
- Sybil attack (multiplication d'identités) : créer de fausses identités sur le réseau afin de le corrompre.
- Sinkhole / Blackhole : intercepter tout le trafic et le router ailleurs (sinkhole) ou ne pas le router (blackhole), nécessite de se faire passer pour la base ou un relayeur.
- Sniffing : écouter le trafic pour l'analyser, permet ensuite de faire du DOS ou MITM.
- Hello spamming : se faire passer pour un nouvel objet et envoyer un nombre important de requête d'appairage, permet de faire du DOS ou d'obtenir la connexion au réseau.
Couche de Perception
La couche de perception permet aux objets d’émettre et de recevoir le trafic, rendant les objets vulnérables à plusieurs attaques :
- Eavesdropping : écouter le trafic.
- Brouillage RF : submerger les bandes de fréquences pour noyer les communications.
- Spoofing : se faire passer pour un nœud valide.
- Bloquer la mise en veille : envoyer des signaux de réveil de manière répétée afin de sur-consommer la batterie des objets.
Couche Physique
Enfin la couche physique, constituée du matériel à proprement parlé, ajoute une attaque à la liste :
- Ajout d'un objet malicieux
OS Petalinux
Le processeur utilisé pour le projet est un Zynq xc7z010 (monté sur une carte Zybo), qui est constitué de deux cœurs de type CPU et d'une partie logique de type FPGA, comme indiqué sur le schéma suivant : https://www.xilinx.com/content/dam/xilinx/imgs/block-diagrams/zynq-mp-core-dual.png
On remarque les interfaces AXI qui permettrons d'établir la communication entre le CPU et le FPGA.
Pour commencer, il faut installer une distribution linux de type petalinux sur la partie CPU du processeur. Diligent propose une distribution qui est adaptée à cette famille board.
- Prérequis
- Vivado WebPack version 2017.2 ou 2017.4, de préférence sur une machine linux (j'ai donc installé une machine virtuelle de type ubuntu sur mon pc).
- Le SDK Xilinx (proposé lors de l'installation de Vivado).
- Le fichier de configuration des cartes installé dans /opt/Xilinx/Vivado/VERSION/data/boards.
- Note
- Les version ne sont pas toujours compatibles entre elles. Par exemple, configurer un projet petalinux de version 2015.4 avec le CLI 2017.4 ne fonctionnera pas puisque le système de fichier utilisé est différent. Dans mon cas j'utilise la version 2017.4 sur tous les outils.
- Les builds avec Vivado et Petalinux sont longs, une machine avec 4 cœurs ou plus est préférable.
- Petalinux utilise des chemin de fichier absolu, le dossier d'un projet petalinux ne peut donc pas être changé d'emplacement.
- Sources
Installation de l'outil petalinux
Petalinux est un outil en ligne commande permettant de compiler des images linux, l'exécutable pour l'installation peut être trouvé ici, il choisir la même version que l'installation de Vivado.
L'installation requiert plusieurs librairies et échouera si l'une d'entre elles n'est pas installée (à exécuter en tant que root):
apt-get install g++ chrpath xvfb xterm tofrodos iproute gawk gcc git-core make net-tools libncurses5-dev tftpd zlib1g-dev libssl-dev flex bison libselinux1 lib32z1 lib32ncurses5 lib32stdc++6 libbz2-1.0:i386 libtool-bin
- L'installation peut être lancée même si il manque l'une de ces librairies, mais elle échouera au bout d'une dizaine de minutes. D'autre part, en cas d'échec de l'installation, le dossier de destination n'est pas vidé, entraînant alors aussi un échec lors d'une nouvelle tentative.
L'installation peut ensuite être lancée (/opt/Petalinux doit exister et disposer d'au moins 20Go):
PATH-TO/petalinux-v2017.4-final-installer-dec.run opt/PetaLinux
Une fois l'installation terminée (ce qui peut prendre environ 15 mins), vérifiez que vous êtes bien configurer en bash et sourcez les scripts nécessaires :
sudo dpkg-reconfigure dash source /opt/Petalinux/settings.sh source /opt/Xilinx/Vivado/2017.4/settings64.sh
(les sources peuvent être ajoutées directement dans /etc/bash.bashrc)
Préparation de la carte SD
L'image linux va être booté depuis une carte SD. Il faut donc placer le jumper JP5 sur la carte Zybo en position SD/QSP1.
La carde SD doit être formatée avec une partition fat32 d'1Go pour le boot et d'une partition ext4 pour le système de fichier.
Pour ce faire il faut supprimer le formatage par défaut de la carte sd. Je l'ai fait depuis le powershell de Windows :
> DISKPART DISKPART > list disk DISKPART > select disk 1 DISKPART > disk clean DISKPART > exit
Ensuite j'ai monté la carte sd sur ma machine virtuelle pour la formater. Pour une machine virtuelle tournant sur VirtualBOX :
- Obtenez le DeviceID du disque (depuis un powershell):
wmic diskdrive list brief
- Creez le fichier vmdk (depuis un powershell en tant qu'administrateur):
C:\Program Files\Oracle\VirtualBox\VBoxManage internalcommands createrawvmdk -filename "%USERPROFILE%/Documents/sdcard.vmdk" -rawdisk "\\.\PHYSICALDRIVE1"
- Ouvrez VirtualBOX en tant qu'administrateur et ajoutez le disque à la machine virtuelle (la vm doit être éteinte):
selectionner la vm > Configuration > Stockage > Contrôleur SATA > Ajoute un disque dur > Choisir un disque existant > ~/Documents/sdcard.vmdk
Pour formater la carte sd avec linux, cherchez la partition (à exécuter en tant que root):
fdisk -l
- Puis lancez l'outil fdisk (en tant que root) pour configurer la carte sd (/dev/sdb dans mon cas):
> fdisk /dev/sdb fdisk > n créer la première partition fdisk > p en fait une primary fdisk > 1 lui affecte le numéro 1 fdisk > premier block (laissez par defaut) fdisk > +1G dernier block pour former une partition d'1Go fdisk > a en fait la partition de boot fdisk > n créer la deuxième partition fdisk > p en fait une primary fdisk > 2 lui affecte le numéro 2 fdisk > premier block (laissez par defaut, après la fin de la première partition) fdisk > dernier block (laissez par defaut pour utiliser l'espace restant) fdisk > w applique les changement fdisk > p affiche les partitions pour vérifier fdisk > q quitter
- Configurez les partitions en fat32 et ext4 (à exécuter en tant que root):
mkfs.vfat -F 32 -n boot /dev/sdb1 mkfs.ext4 -L root /dev/sdb2
Projet petalinux à partir d'un fichier bsp (board support package)
Diligent propose propose une image petalinux simple à utiliser. Elle peut être téléchargée ici.
- Créez le projet à partir du fichier téléchargé (cela peut prendre un certain temps):
petalinux-create -t project -s Petalinux-Zybo-2017.4-1.bsp
Il y a une images précompilée dans le dossier Petalinux-Zybo-2017.4-1/pre-built/linux/images/, copiez BOOT.BIN et image.ub dans la première partition de la carte sd. Placez la carte sd dans la Zybo et mettez la carte sous tension.
Pour se connecter à la carte, utilisez minicom sur le port série (à exécutez en root):
minicom -D /dev/ttyUSB1
Le port série doit être configuré de la façon suivante:
- baud rate = 115200
- data bits = 8
- stop bits = 1
- flow control = none
- parity = none
Le shell du petalinux devrait apparaître. Pour revoir le boot, cliquez sur le bouton BTN7 (PS-SRST) de la carte.
Il est possible de programmer la partie FPGA depuis petalinux en écrivant le bitstream dans /dev/xdevcfg :
cat bitstream.bit > /dev/xdevcfg
Néanmoins, dans mon cas, ceci écrase la configuration de petalinux et le rend inutilisable.
Cette image petalinux est adaptée à une définition hardware (block design) prédéfinie qui est la suivante :
Si on tente de compiler le kernel linux avec un block design différent, le device tree ne correspondra pas et la compilation échouera.
Il faut donc créer sont propre projet petalinux pour y intégrer un block design custom.
Projet petalinux standalone
Il est possible de créer une image Petalinux à partir d'un projet Vivado, incluant alors la partie Hardware (block design), la partie FPGA (bitstream), ainsi que la partie linux (FSBL). Pour commencer, je me suis basé sur un tutoriel de Digilent pour créer un IP utilisant l'interface AXI.
Projet Vivado
On commence par créer un projet dans vivado de type RTL, sans ajouter de module verilog initial, avec un fichier de contrainte (contraintes.xdc) et en choisissant la carte zybo (le fichier de configuration de cartes doit être installé).
Avant de créer de définir la partie hardware, il faut créer l'IP que nous allons utiliser (l'IP sera une PWM simple pilotant les leds). Pour ce faire, il faut se rendre dans "Tools > Create and Package New IP". Dans la nouvelle fenêtre, il faut choisir "Create a new AXI4 peripheral" (AXI étant une interface permettant de faire communiquer le CPU et le FPGA). Cela va créer un repository local contenant cet IP (et potentiellement les prochains IPs), ce repository peut être utilisé dans d'autres projets.
On peut ensuite configurer l'IP, dans ce cas on utilisera l'interface AXI lite en mode slave (puisqu'il sera piloté par le CPU) avec un bus de 32 bits et 4 registres (on notera qu'il y a une option pour inclure les interruptions) :
Maintenant l'IP créer, on remarque deux fichier verilog ({IP_NAME}_v1_0.v et {IP_NAME}_v1_0_S00_AXI_inst.v), il faut leur ajouter la partie logique. Puisque la PWM pilote les leds, les ports à ajouter sont les 4 leds (à modifier dans les 2 fichiers) :
// Users to add ports here output wire PWM0, output wire PWM1, output wire PWM2, output wire PWM3, // User ports ends
On ajoute aussi le paramètre d'amplitude de la PWM dans les 2 fichiers (ce paramètre pourra être modifier dans le block design du projet) :
// Users to add parameters here parameter integer PWM_COUNTER_MAX = 1024, // User parameters ends
La partie logique (dans le fichier _inst.v) :
// Add user logic here //registre définissant l'amplitude de la PWM reg [15:0] counter = 0; //incrémente le registre à chaque front montant de la clock de l'IP always @(posedge S_AXI_ACLK) begin if(counter < PWM_COUNTER_MAX-1) begin counter <= counter + 1; else //réinitialise le registre si il est égale au paramètre PWM_COUNTER_MAX counter <= 0; end end //signal PWM //slv_reg correspont au registres d'AXI lites créer précédemment //si la valeur du registre slave est inférieure au compteur, l'ouput est inactif (1'b0), sinon il est actif //la valeur du registre slave sera modifié par le code c grâce au memory mapping assign PWM0 = slv_reg0 < counter ? 1'b0 : 1'b1; assign PWM1 = slv_reg1 < counter ? 1'b0 : 1'b1; assign PWM2 = slv_reg2 < counter ? 1'b0 : 1'b1; assign PWM3 = slv_reg3 < counter ? 1'b0 : 1'b1; // User logic ends
Enfin on modifie la définition du bus pour y inclure nos variables (dans le fichier {IP_NAME}_v1_0) :
// Instantiation of Axi Bus Interface S00_AXI My_PWM_Core_v1_0_S00_AXI # ( .C_S_AXI_DATA_WIDTH(C_S00_AXI_DATA_WIDTH), .C_S_AXI_ADDR_WIDTH(C_S00_AXI_ADDR_WIDTH), .PWM_COUNTER_MAX(PWM_COUNTER_MAX) ) My_PWM_Core_v1_0_S00_AXI_inst ( .PWM0(PWM0), .PWM1(PWM1), .PWM2(PWM2), .PWM3(PWM3), .S_AXI_ACLK(s00_axi_aclk), .S_AXI_ARESETN(s00_axi_aresetn), .S_AXI_AWADDR(s00_axi_awaddr), .S_AXI_AWPROT(s00_axi_awprot), .S_AXI_AWVALID(s00_axi_awvalid), .S_AXI_AWREADY(s00_axi_awready), .S_AXI_WDATA(s00_axi_wdata), .S_AXI_WSTRB(s00_axi_wstrb), .S_AXI_WVALID(s00_axi_wvalid), .S_AXI_WREADY(s00_axi_wready), .S_AXI_BRESP(s00_axi_bresp), .S_AXI_BVALID(s00_axi_bvalid), .S_AXI_BREADY(s00_axi_bready), .S_AXI_ARADDR(s00_axi_araddr), .S_AXI_ARPROT(s00_axi_arprot), .S_AXI_ARVALID(s00_axi_arvalid), .S_AXI_ARREADY(s00_axi_arready), .S_AXI_RDATA(s00_axi_rdata), .S_AXI_RRESP(s00_axi_rresp), .S_AXI_RVALID(s00_axi_rvalid), .S_AXI_RREADY(s00_axi_rready) );
L'IP est presque prêt à être utilisé, il faut configurer le paramètre PWM_COUNTER_MAX pour qu'il puisse être modifier dans le block design du projet. Pour cela, il faut se rendre dans l'onglet "Package IP", puis dans "Customization Parameters" et cliquer sur "Merge Changes from Customization Parameters Wizard". Se rendre dans "Customization GUI" double cliquer sur le paramètre et cocher "Visible in Customization GUI", puis le faire glisser avec les autres paramètres dans "Page 0". Bien sûr, si le paramètre n'a pas besoin d'être modifié depuis le block desgin, cette étape peut être passée.
Enfin, se rendre dans l'onglet "Review and Package" et cliquer sur "Re-Package IP". La fenêtre va alors se fermer et on peut créer notre block design (qui définie la partie hardware).
On crée un nouveau block design et on y ajoute l'IP ZYNQ7 Processing System ainsi que l'IP que l'on a crée ci-dessus. Les connections étant simples on peut utiliser "Run Block Automation" et "Run Connection Automation", puis ajouter les outputs PWM0 à PWM3 en faisant un clique droit sur la variable du même nom (sur l'IP) et "Create PORT". Le block design devrait être comme suit :
On peut maintenant créer le module verilog associé au block design, en faisant un clique droit sur le fichier du block design (dans l'onglet "Sources") et en choissisant "Create HDL Wrapper".
Pour finir, il faut éditer le fichier de contraintes (contraintes.xdc) pour définir les pins des leds à utiliser :
##IO_L23P_T3_35 set_property PACKAGE_PIN M14 [get_ports PWM0] set_property IOSTANDARD LVCMOS33 [get_ports PWM0] ##IO_L23N_T3_35 set_property PACKAGE_PIN M15 [get_ports PWM1] set_property IOSTANDARD LVCMOS33 [get_ports PWM1] ##IO_0_35 set_property PACKAGE_PIN G14 [get_ports PWM2] set_property IOSTANDARD LVCMOS33 [get_ports PWM2] ##IO_L3N_T0_DQS_AD1N_35 set_property PACKAGE_PIN D18 [get_ports PWM3] set_property IOSTANDARD LVCMOS33 [get_ports PWM3]
Le fichier de contraintes de base de la Zybo peut être trouvé à cette adresse
La partie sur Vivado est maintenant terminée, et on peut passer à la partie XSDK. Pour ce faire cliquez sur File > Export > Export Hardware > cochez "Include Bitstream" > OK. Puis File > Launch SDK > OK.
Xilinx SDK
Tout d'abord, on remarque bien la présence de notre wrapper dans les sources :
Cette structure a été créée lorsque nous avons exporté la partie hardware.
Il faut maintenant concevoir le code C qui va piloter l'IP. Créez une nouvelle application en allant dans File > New > Application Project. Donnez lui un nom et vérifiez que "Hardware Platform" est bien le dossier vu sur l'image ci-dessus. Cliquez sur next puis choisissez le template "Hello World", cela permettra d'inclure les librairies nécessaires. Enfin supprimez helloworld.c dans l'explorateur de fichier et créez un fichier main.c avec le contenu suivant :
#include "xparameters.h" #include "xil_io.h" #define MY_PWM 0x43C00000 //This value is found in the Address editor tab in Vivado (next to Diagram tab) int main(){ int num=0; int i; while(1){ if(num == 256) num = 0; else num++; //écrit dans le registre slv_reg0 Xil_Out32(MY_PWM, num); //écrit dans le registre slv_reg1 Xil_Out32((MY_PWM+4), num); //écrit dans le registre slv_reg2 Xil_Out32((MY_PWM+8), num); //écrit dans le registre slv_reg3 Xil_Out32((MY_PWM+12), num); for(i=0;i<300000; i++); } }
Sauvegardez (le code est build automatiquement).
On peut maintenant tester le fonctionnement. D'abord, il faut placer le jumper JP5 de la Zybo sur JTAG (si ce n'est pas déjà le cas).
On programme la partie FPGA : Xilinx > Program FPGA, verifiez que le bitstream soit le bon et cliquez sur Program. Les leds devraient s'allumer et le rester (c'est normal puisque seul la partie FPGA est programmée, donc les registres slaves de notre IP ont toujours la même valeur). Ensuite, lancez l'application C, clique droit sur le projet (pas le bsp) > Run As > Launch on Hardware (System Debugger). La PWM entre en action et les leds devraient "pulser".
Création d'une image bootable
Pour le moment l'application est chargée directement en RAM, donc lorsque la Zybo est mise hors tension toutes les configurations précédentes sont perdues. Pour que l'application soit présente à la mise sous tension de la carte, il faut créer une image bootable.
Il faut commencer par créer le FSBL (first stage boot loader) qui va donner les instructions au CPU à la mise sous tension. Dans XSDK, cliquez sur File > New > Application Project, donnez un nom au projet (par ex: FSBL), vérifiez que "Hardware Platform" est le bon dossier, cliquez sur Next puis choisissez le template "Zynq FSBL". Le projet devrait être build automatiquement, on peut maintenant créer l'image bootable.
Toujours dans XSDK, cliquez sur Xilinx > Create Boot Image. Dans la nouvelle fenêtre, choisissez le dossier de destination. Dans la partie "Boot image partitions", cliquez sur Add et cherchez FSBL.elf dans la section File path (le fichier se trouve dans VIVADO_PROJECT/VIVADO_PROJECT.sdk/FSBL/Debug), choisissez "bootloader" dans Partition type. Répétez l'opération pour le bitstream et le .elf de l'application C avec le type "datafile" :
- N.B. L'ordre des fichiers dans Boot image partitions doit être respecter.
Enfin, placez le fichier BOOT.BIN qui vient d'être généré dans la première partition de la carte SD. Placez le jumper JP5 de la Zybo sur SD (si ce n'est pas déjà le cas) et mettez la carte sous tension. Les leds devraient avoir le même comportement vu précédemment, mais cette fois ci l'application est exécuté à la mise sous tension de la carte.
Création de l'image Petalinux
A ce stade, nous avons une image bootable, mais aucun OS. Nous allons donc compiler une image petalinux.
- N.B. Petalinux étant utilisé sous Linux, l'intégralité du projet Vivado et XSDK doit être réalisée sur Linux, il y aura sinon des conflits lors de la compilation du kernel à cause du wrapper hardware qui n'est pas le même selon les OS.
Pour commencer, il faut créer un projet petalinux:
petalinux-create -t project --name petalinux-zybo --template zynq cd petalinux-zybo/
Ensuite, décrire la partie hardware:
petalinux-config --get-hw-description VIVADO_PROJECT/VIADO_PROJECT.sdk/
- cela peut prendre du temps (~15 mins sur une machine dual core)
Puis configurer le kernel et le système de fichier:
petalinux-config -c rootfs petalinux-config -c kernel
- cela va générer des fichiers permettant la compilation (~30 mins sur une machine dual core)
Dans mon cas, j'ai laisser la configuration par défaut du kernel et copier le fichier de configuration obtenu grâce au bsp (vu dans la partie Projet petalinux à partir d'un fichier bsp (board support package)) afin d'avoir un système de fichier propre:
cp -r ../PETALINUX_FROM_BSP_FOLDER/project-spec/configs ./project-spec
Enfin, petalinux peut être build:
petalinux-build
- (~30 mins sur une machine dual core)
On peut maintenant créer le fichier BOOT.BIN:
petalinux-package --boot --format BIN --fsbl VIVADO_PROJECT/VIVADO_PROJECT.sdk/FSBL/Debug/FSBL.elf --fpga VIVADO_PROJECT/VIVADO_PROJECT.sdk/design_1_wrapper_hw_platform_0/design_1_wrapper.bit --u-boot --boot-device sd
On dispose alors de ./BOOT.BIN ainsi que ./images/linux/image.ub (créé lors de petalinux-build et décrivant le système de fichier) que l'on peut placer dans la première partition de la carde SD pour booter petalinux sur la Zybo.
Cependant, nous n'avons pas notre application C dans le système de fichier, il faut donc le faire.
D'abord il faut créer l'application avec petalinux:
petalinux-create -t apps --name sampleapp --template install --enable
- il existe aussi des templates pour c et c++ permettant de mettre les fichiers sources couplés à un Makefile pour recompiler l'application à chaque build
Ensuite on place l’exécutable dans le dossier adéquat:
cp VIVADO_PROJECT/VIVADO_PROJECT.sdk/APP_NAME/Debug/APP_NAME.elf ./project-spec/meta-user/recipes-apps/sampleapp/files/sampleapp
- il faut renommer le fichier .elf pour qu'il ai le même nom que l'application créer avec petalinux-create -t apps
On relance la commande petalinux-build (cette fois ci cela devrait durer environ 5 mins) puis petalinux-package, on place BOOT.BIN et image.ub sur la première carte sd. Une fois la Zybo mise sous tension, on peut s'y connecter via minicom (ou putty, tera term, ...) pour accéder au shell du petalinux. root/root pour s'y connecter en root, et on remarque bien la présence de notre executable dans /usr/bin. Cependant, si on tente de l'exécuter:
/usr/bin/sampleapp
On observe que la console retourne:
Illegal instruction
En fait, lors de la création de l'application sur XSDK, le projet est par défaut en OS "standalone", les drivers ajoutés par XSDK ne fonctionnent donc que sur application tournant sur du "baremetal" (sans Linux derrière).
Il faut donc créer un projet en choisissant un OS Linux, importer les librairies qui vont biens (voire en écrire dans certains cas), et compiler des drivers pour que Linux puisse utiliser l'hardware de la Zybo.
J'ai aussi préparé une autre image incluant l'application :
petalinux-package --boot --format BIN --fsbl VIVADO_PROJECT/VIVADO_PROJECT.sdk/FSBL/Debug/FSBL.elf --add VIVADO_PROJECT/VIVADO_PROJECT.sdk/PWM/Debug/PWM.elf --fpga VIVADO_PROJECT/VIVADO_PROJECT.sdk/design_1_wrapper_hw_platform_0/design_1_wrapper.bit --u-boot --boot-device sd
Mais cela a revient à faire l'opération sur XSDK vu dans la partie "Création d'une image bootable". L'application fonctionne, mais il n'y a aucun OS.
Kernel Module & Device Driver
Comme on l'a vu dans la partie précédente, il faut pouvoir décrire le matériel "hardware" de la Zybo pour qu'il puisse être utilisé par Linux. Il existe deux méthodes pour réaliser ce genre de chose :
- memory mapping (mmap)
- Dans ce premier cas, il faut utiliser le combo RAM virtuelle (le fichier /dev/mem) + mmap() (fonction C) pour écrire en RAM à l'adresse du matériel que l'on veut piloter. Il y a cependant deux gros inconvénients à cette méthode, il faut le privilège root, et les interruptions ne sont pas supportées. L'avantage est qu'il n'y a en fait pas besoin d'écrire de module kernel, ce qui la rend donc plus simple à mettre en place.
- Userspace I/O (UIO)
- Ici, chaque device aura son espace dans /dev (/dev/uio0, /dev/uio1, ...) qui permettra de le piloter. L'avantage est que les interruptions sont supportées. En revanche il s'agit cette fois d'un module kernel, qui est donc plus dur à mettre en place.
Test de memory mapping avec /dev/mem
Pour tester le memory mapping, j'ai simplement garder le projet précédent (la PWM) et utilisé un code en C : devmem.c.
Ce code C doit être compilé et son exécutable placé dans le système de fichier de petalinux :
petalinux-create -t apps --name devmem --template c --enable cd project-spec/meta-user/recipes-apps/devmem rm Makefile Readme Kconfig cp PATH_TO/devmem.c ./files/ touch ./files/Makefile vi ./files/Makefile APP = devmem # Add any other object files to this list below APP_OBJS = devmem.o all: $(APP) $(APP): $(APP_OBJS) $(CC) $(LDFLAGS) -o $@ $(APP_OBJS) $(LDLIBS) clean: -rm -f $(APP) *.elf *.gdb *.o vi devmem.bb SUMMARY = "devmem application" SECTION = "PETALINUX/apps" LICENSE = "MIT" LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302" SRC_URI = "file://devmem.c \ file://Makefile \ " S = "${WORKDIR}" CFLAGS_prepend = "-I ${S}/include" do_compile() { oe_runmake } do_install() { install -d ${D}${bindir} install -m 0755 ${S}/devmem ${D}${bindir} } petalinux-build
Comme vu auparavant, j'ai généré BOOT.BIN, et placé BOOT.BIN et image.ub sur la carte SD.
Une fois dans le shell de la Zybo :
/usr/bin/devmem 0x43C00000 > GPIO access through /dev/mem. > gpio dev-mem test: input: 00000000
Ici, 0x43C00000 correspond au premier registre de l'IP PWM (soit slv_reg0). Sa valeur est de 0.
Si on écrit dans le registre :
/usr/bin/devmem 0x43C00000 -o 256
L'une des leds s'allume avec une intensité réduite, probablement 1/4 de son intensité puisque le paramètre définie dans l'IP fixait le maximum à 1024. On peut tester en écrivant la valeur maximale et au dessus :
/usr/bin/devmem 0x43C00000 -o 1024 /usr/bin/devmem 0x43C00000 -o 2048
Dans le premier cas la led semble être à son intensité maximale, dans le second cas, l'intensité n'a pas changé. Ce qui correspond bien à notre IP PWM.
Les autres registres (slv_reg1, 2 et 3) se trouvent aux adresses 0x43C00004, 0x43C00008 et 0x43C0000C et pilotent bien les leds restantes.
En combinant devmem.c et main.c (de l'application créée sur XSDK), j'obtient :
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> #include <fcntl.h> #define SLV_0 0x43C00000 #define SLV_1 0x43C00004 #define SLV_2 0x43C00008 #define SLV_3 0x43C0000C int gpio_driver(unsigned gpio_addr, int value) { int fd; unsigned page_addr, page_offset; void *ptr; unsigned page_size=sysconf(_SC_PAGESIZE); /* Open /dev/mem file */ fd = open ("/dev/mem", O_RDWR); if (fd < 1) { return -1; } /* mmap the device into memory */ page_addr = (gpio_addr & (~(page_size-1))); page_offset = gpio_addr - page_addr; ptr = mmap(NULL, page_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, page_addr); /* Write value to the device register */ *((unsigned *)(ptr + page_offset)) = value; munmap(ptr, page_size); return 0; } int main(){ int num=0; int i; while(1){ if(num == 256) { num = 0; } else { num++; } gpio_driver(SLV_0, num); gpio_driver(SLV_1, num); gpio_driver(SLV_2, num); gpio_driver(SLV_3, num); usleep(10000); } }
Puis je le compile et l'installe dans petalinux de la même manière que devmem.c :
petalinux-create -t apps --name pwm --template c --enable
Encore une fois, je génère BOOT.BIN le copie avec image.ub sur la carte SD. Puis j'essaye d'exécuter l'application :
/usr/bin/pwm
Les leds montent lentement à leur intensité maximale, mais une fois cette dernière atteinte, elles restent dans cet état.
UIO device driver
Pour tester les interruptions du FPGA vers le CPU, j'ai crée un simple module dans la partie FPGA qui va envoyer une interruption au processeur chaque fois qu'un bouton est pressé.
On commence par créer un nouveau projet RTL dans Vivado (avec un fichier de contraintes et en choisissant la board Zybo). Avant de créer le block design, il faut créer un IP qui générera les interruptions (Tools > Create and Package New IP), avec une interface AXI Lite slave par défaut (qui servira juste à tester la lecture / écriture par UIO).
Cette fois ci, on ne touche pas au fichier _AXI_inst.v, la logique sera codée dans le fichier de haut niveau du module (IP_NAME.v).
Il faut ajouter les ports nécessaires :
// Users to add ports here input wire [3 : 0] btns, output wire interrupt, // User ports ends
Puis la logique :
// Add user logic here integer interrupt_flag = 0; assign interrupt = interrupt_flag; always @ (posedge s00_axi_aclk) begin interrupt_flag <= btns[0] || btns[1] || btns[2] || btns[3]; end // User logic ends
Une fois l'IP terminé, on peut passer au block design, il faut d'abord ajouter l'IP Zynq Processing System et autoriser les interruption depuis le FPGA (double clique sur l'IP, onglet Interrupts) :
Ensuite on ajoute l'IP créer précédemment, puis on peut utiliser les outils Run block automation et Run connection automation, enfin il faut connecter la sortie interrupt de l'IP à l'entrée IRQ_FP2 du processeur et créer le port pour les boutons (clique droit sur la varibale > Create Port) :
Une fois le design validé et le wrapper crée, le fichier de contraintes doit être édité comme suit :
##Buttons ##IO_L20N_T3_34 set_property PACKAGE_PIN R18 [get_ports {btns[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {btns[0]}] ##IO_L24N_T3_34 set_property PACKAGE_PIN P16 [get_ports {btns[1]}] set_property IOSTANDARD LVCMOS33 [get_ports {btns[1]}] ##IO_L18P_T2_34 set_property PACKAGE_PIN V16 [get_ports {btns[2]}] set_property IOSTANDARD LVCMOS33 [get_ports {btns[2]}] ##IO_L7P_T1_34 set_property PACKAGE_PIN Y16 [get_ports {btns[3]}] set_property IOSTANDARD LVCMOS33 [get_ports {btns[3]}]
Puis le bitstream peut être généré avant d'exporter la partie hardware puis lancer le sdk pour générer le FSBL (comme déjà vu auparavant).
Memory mapping
De la même manière qu'avec /dev/mem, il est possible d'ouvrir le fichier du device driver /dev/uio* pour ensuite obtenir un pointeur vers la plage mémoire utilisé par le device :
void main() { int uiofd = open("/dev/uio0", O_RDWR); if(uiofd < 0) { printf("Error opening uio device\n\r"); exit(EXIT_FAILURE); } void* ptr = mmap(NULL, 0x10000, PROT_READ|PROT_WRITE, MAP_SHARED, uiofd, 0); // 0x10000 = 65536, default size for axi slave interface *((unsigned *)(ptr + 0x04)) = 0xffffffff; // write to slv_reg1 munmap(ptr, 0x10000); close(uiofd); exit(EXIT_SUCCESS); }
Interruptions
Sources
- https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18842490/Testing+UIO+with+Interrupt+on+Zynq+Ultrascale
- https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18842191/GIC
- https://yurovsky.github.io/2014/10/10/linux-uio-gpio-interrupt.html
J'ai utilisé un autre projet petalinux pour tester les interruptions :
petalinux-create -t project --name uio --template zynq cd uio/ petalinux-config --get-hw-description VIVADO_PROJECT/VIVADO_PROJECT.sdk petalinux-config -c rootfs petalinux-config -c kernel
Dans le menu de configuration du kernel, il faut se rendre dans Device Drivers > Userspace I/O drivers et inclure :
- Userspace I/O platform driver with generic IRQ handling
- Userspace platform driver with generic irq and dynamic memory
puis dans Boot options et inclure Use appended device tree blob to zImage.
Maintenant, il faut configurer le device tree.
Editer projet-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi :
/include/ "system-conf.dtsi" / { chosen { bootargs = "console=ttyPS0,115200 earlyprintk uio_pdrv_genirq.of_id=generic-uio root=/dev/mmcblk0p2 rw rootwait"; }; }; &myint_0 { compatible = "generic-uio"; interrupts = <0 29 1>; };
myint_0 fait référence à un nœud dans cat components/plnx_workspace/device-tree/device-tree-generation/pl.dtsi :
/ { amba_pl: amba_pl { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; ranges ; myint_0: myint@43c00000 { compatible = "xlnx,myint-1.0"; interrupt-parent = <&intc>; interrupts = <0 29 4>; reg = <0x43c00000 0x10000>; xlnx,s00-axi-addr-width = <0x4>; xlnx,s00-axi-data-width = <0x20>; }; }; };
Dans reg, 0x43C00000 est la première adresse et 0x10000 la taille des registres (64Kb) Dans interrupts, 0 indique que l'interruption est de type SPI. 29 correspond au port d'interruption. L'entrée IRQ_FP2 est un bus [15:0] mappé sur [91:84] et [68:61] (comme on peut le voir sur l'image au desssus). On peut voir sur le block design qu'on utilise IRQ_FP2[0:0], donc mappé sur 61 auquel il faut soustraire 32 (convention de syntax d'un device tree, il faut soustraire 16 si le premier flag est 1). J'ai changer le 4 en 1 dans la partie interrupts pour que l'interruption se fasse sur un front montant au lieu d'un 1 logique constant.
Enfin, on peut lancer le build et générer l'image :
petalinux-build petalinux-package
Après avoir booté petalinux sur la Zybo, on vérifie qu'il y a bien notre UIO :
ls /dev/uio* > /dev/uio0 ls /sys/class/uio/uio0/ > dev device event maps name power subsystem uevent version
Le fichier /dev/uio0 permet d'accéder à la mémoire mappée pour le device en question. Cela évite de devoir passer par /dev/mem.
Et notre interruption de configurée :
cat /proc/interrupts | grep myint > 46: 0 0 GIC-0 61 Edge myint
Où "0 0" représente le nombre d'interruptions perçues respectivement par le cœur 0 et le cœur 1.
Après avoir appuyé sur un des boutons de la Zybo, une interruption a bien été générée :
cat /proc/interrupts | grep myint > 46: 1 0 GIC-0 61 Edge myint
Il faut maintenant créer une application pour traiter les interruptions :
petalinux-create -t apps --name uioint --template c --enable
Editer project-spec/meta-user/recipes-apps/uioint/files/uioint.c :
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> int main(void) { int fd = open("/dev/uio0", O_RDWR); if (fd < 0) { perror("open"); exit(EXIT_FAILURE); } while (1) { uint32_t info = 1; // unmask ssize_t nb = write(fd, &info, sizeof(info)); if (nb != (ssize_t)sizeof(info)) { perror("write"); close(fd); exit(EXIT_FAILURE); } //Wait for interrupt nb = read(fd, &info, sizeof(info)); if (nb == (ssize_t)sizeof(info)) { printf("Interrupt #%u!\n", info); } } close(fd); exit(EXIT_SUCCESS); }
Après avoir buildé pétalinux et booté dessus sur la Zybo :
uioint > //boucle infinie > Interrupt #1! //j'appuie sur un bouton de la Zybo > Interrupt #2! //encore
Et dans /proc/interrupts :
cat /proc/interrupts | grep myint > 46: 2 0 GIC-0 61 Edge myint
Il faut tout de même noté que cela n'est pas une vraie interruption, ce n'est pas le kernel qui invoque un handler, mais une fonction qui lit en boucle un registre.
Interruption avec un module kernel
Notes
- J'ai repris le même projet Vivado et le même device tree que dans la partie précédente.
- Attention au bootarg "uio_pdrv_genirq.of_id=generic-uio" dans system-user.dtsi. Cet argument permet d'instancier le device dans un UIO, mais il génère aussi un handler générique pour l'interruption et enpêche donc d'y en associer un autre.
Sources
- https://notes.shichao.io/lkd/ch7/#registering-an-interrupt-handler
- https://www.kernel.org/doc/html/v4.12/core-api/genericirq.html
Pour traiter les interruptions, il faut ajouter un module kernel à petalinux :
petalinux-create -t modules --name myint --enable
Puis éditer project-spec/meta-user/recipes-modules/myint/files/myint.c :
#include <linux/kernel.h> #include <linux/module.h> #include <linux/interrupt.h> #include <linux/irq.h> #include <linux/slab.h> // find in /proc/interrupts #define IRQ_NUM 46 static irqreturn_t myint_handler(int irq, void *dev_id) { printk("Interrupt Occurred : Button Pressed !"); return IRQ_HANDLED; } static int __init myint_init(void) { //Register ISR int req_status = request_irq(IRQ_NUM, myint_handler, 0, "myint", NULL); if (req_status) { printk(KERN_ERR "myint_init: Cannot register IRQ %d\n", IRQ_NUM); return req_status; } else { printk(KERN_INFO "myint_init: Registered IRQ %d\n", IRQ_NUM); } return 0; } static void __exit myint_edit(void) { //free irq free_irq(IRQ_NUM, NULL); } module_init(myint_init); module_exit(myint_edit); MODULE_LICENSE("GPL");
- N.B le MODULE_LICENSE est important pour importer les librairies.
Cette fois l'interruption n'est pas répertoriée dans /proc/interrupts. Il faut d'abord lancer le module kernel :
insmod /lib/modules/4.9.0-xilinx-v2017.4/extra/myint.ko > myint_init: registred IRQ 46
On appuie quelque fois sur les boutons de la Zybo :
> Interrupt Occured : Button Pressed ! > Interrupt Occured : Button Pressed ! > Interrupt Occured : Button Pressed ! ...
Et dans /proc/interrupts :
cat /proc/interrupts | grep myint > 46: 13 0 GIC-0 61 Edge myint
Modules FPGA
Module 1
Le premier module FPGA consiste en un simple acquéreur de données provenant des PMODs qui envoie une interruption au CPU si l'une d'entre elles dépasse un seuil prédéfinie. Pour ce faire je vais utiliser un IP custom avec une interface AXI Lite disposant d'au moins 3 registres de 32 bits (1 pour la configuration, 1 pour stocker le seuil, et le dernier pour stocker la valeur). Pour chaque donnée à surveiller, il faudra ajouter 2 registres (pour le seuil et la valeur).
Notes
- A chaque fois que l'IP custom est modifié, il faut exécuter les étapes suivantes :
- Upgrade l'IP dans le block design
- Générer le wrapper hdl
- Générer le bitstream
- Exporter la partie hardware
- Rebuild le FSBL (dans XSDK)
- Générer BOOT.BIN (avec petalinux)
Création de l'IP custom
On commence par créer un nouveau projet puis un IP AXI4 disposant d'une interface AXI4-slave (comme dans cette partie).
On ajoute la partie directement dans le fichier _inst.v (car nous allons modifier l’instanciation du protocole AXI lite).
- Définition des ports :
// Users to add ports here input wire [7 : 0] PMOD, input wire [3 : 0] SWITCHES, output wire [3 : 0] LEDS, output wire INTERRUPT, // User ports ends
- Pour tester, nous utiliserons les switches de la Zybo. Ainsi que les leds, dont l'allumage correspondra au 4 derniers bits du registres contenant la valeur.
- Changement du nom des registres :
//-- Number of Slave Registers 4 reg [C_S_AXI_DATA_WIDTH-1:0] source; reg [C_S_AXI_DATA_WIDTH-1:0] threshold_0; reg [C_S_AXI_DATA_WIDTH-1:0] value_0; reg [C_S_AXI_DATA_WIDTH-1:0] channel;
- Cette étape est uniquement pour avoir plus de clarté dans le code.
- Changement de l'instanciation du protocole AXI :
always @( posedge S_AXI_ACLK ) begin if ( S_AXI_ARESETN == 1'b0 ) begin //reset source <= 0; threshold_0 <= 0; value_0 <= 0; channel <= 0; end else begin if (slv_reg_wren) begin case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] ) //cpu has written in source register 2'h0: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin source[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end //cpu has written in threshold_0 register 2'h1: for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 ) if ( S_AXI_WSTRB[byte_index] == 1 ) begin threshold_0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8]; end default : begin source <= source; threshold_0 <= threshold_0; end endcase end end end
- Avec cette modification, le CPU ne peut plus écrire dans les registres value_O et channel, ils sont réservé à l'usage du FPGA. En revanche le CPU peut toujours lire dedans.
- Ajout de la partie logique utilisateur :
// Add user logic here reg [32 : 0] data_out = 0; reg[3 : 0] data_led = 0; integer interrupt_flag = 0; assign LEDS = data_led; //assign leds register to leds wire assign INTERRUPT = interrupt_flag; //assign interrupt value to its wire always @( posedge S_AXI_ACLK ) begin data_out = 32'b0; //reset data register if (source == 32'h504d4f44) begin //source == PMOD value_0 <= {data_out[31 : 8], PMOD[7 : 0]}; end if (source == 32'h42544e53) begin //source = BTNS value_0 <= {data_out[31 : 4], SWITCHES[3 : 0]}; end end always @( posedge S_AXI_ACLK ) begin interrupt_flag = 0; //reset interruption vlaue data_led[3 : 0] <= value_0[3 : 0]; //put leds on/off according to the last 4 bits value of the value_0 register if (value_0 > threshold_0) begin channel <= 32'h0; //interruption source (value_0) interrupt_flag = 1; //send an interruption end end // User logic ends
Il faut ensuite modifier le fichier de haut niveau pour y inclure nos ports :
// Users to add ports here input wire [7 : 0] pmod, input wire [3 : 0] switches, output wire [3 : 0] leds, output wire interrupt, // User ports ends ... ) module1_v1_0_S00_AXI_inst ( .SWITCHES(switches), .PMOD(pmod), .LEDS(leds), .INTERRUPT(interrupt), ...
On package l'IP, on retourne sur le projet vivado, et on crée le block design comme suit :
- N.B Il faut autoriser les interruptions dans l'IP zynq (comme vu dans cette partie)
Une fois le bitstream généré, et l'hardware exporté, il faut créer le FSBL dans XSDK.
Creation de l'applicaiton dans petalinux
On commence par créer un projet petalinux permettant d'utiliser les interruptions (comme vu ici).
Le fichier projet-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi doit être éditer :
/include/ "system-conf.dtsi" / { chosen { bootargs = "console=ttyPS0,115200 earlyprintk uio_pdrv_genirq.of_id=generic-uio root=/dev/mmcblk0p2 rw rootwait"; }; }; &module1_0 { compatible = "generic-uio"; interrupts = <0 29 1>; };
- module1_0 correspond au nom de l'IP créé précédemment.
Puis on crée deux applications c :
petalinux-create -t apps --template c --enable --name module1
- N.B. Dans le Makefile, il faut ajouter l'instruction clean. Sinon, si le code est changé et que petalinux tente de recompilé, il va retourner une erreur car il ne peut pas faire de make clean.
- project-spec/meta-user/recipes-apps/module1/files/module1.c :
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <sys/mman.h> #include <fcntl.h> #include <unistd.h> #include <time.h> #include <signal.h> #include <string.h> #define SOURCE 0x42544e53 // BTNS //#define SOURCE 0x504d4f44 // PMOD #define THRESHOLD 12 // threshold value, if value_0 register exceeds it, an interruption is sent #define MODULE1_UIOD "/dev/uio0" // module1 uio device driver location #define MAP_SIZE 0x10000 // found in vivado address editor // found in xilinx AXI documentation #define SOURCE_OFFSET 0x00 #define THRESHOLD_0_OFFSET 0x04 #define VALUE_0_OFFSET 0x08 #define CHANNEL_OFFSET 0x0C volatile sig_atomic_t CONTINU = 1; void signal_handler(int signum, siginfo_t *info, void *ptr) { CONTINU = 0; } void catch_signals() { static struct sigaction _sigacterm; static struct sigaction _sigactint; memset(&_sigacterm, 0, sizeof(_sigacterm)); _sigacterm.sa_sigaction = signal_handler; _sigacterm.sa_flags = SA_SIGINFO; memset(&_sigactint, 0, sizeof(_sigactint)); _sigactint.sa_sigaction = signal_handler; _sigactint.sa_flags = SA_SIGINFO; sigaction(SIGINT, &_sigactint, NULL); sigaction(SIGTERM, &_sigacterm, NULL); } void main() { time_t t; struct tm tm; FILE* fd; int uiofd; unsigned int value, channel, threshold; uint32_t info; ssize_t nb; printf("Start of module 1 process\n\r"); // open uio device driver uiofd = open(MODULE1_UIOD, O_RDWR); if(uiofd < 0) { printf("Error opening uio device\n\r"); exit(EXIT_FAILURE); } // get a pointer to memory mapped device void* ptr = mmap(NULL, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, uiofd, 0); // open a file to store values return by the PL fd = fopen("./channel0.csv","w"); if(fd < 0) { printf("Error opening csv file\n\r"); exit(EXIT_FAILURE); } catch_signals(); // tell the PL module to use BTNS as source *((unsigned *)(ptr + SOURCE_OFFSET)) = SOURCE; printf("Source setted\n\r"); // tell the PL module the treshold *((unsigned *)(ptr + THRESHOLD_0_OFFSET)) = THRESHOLD; printf("threshold setted to %d\n", THRESHOLD); // init csv headers fprintf(fd,"\"timestamp\",\"value\""); if(fork() == 0) { // subprocess while (CONTINU) { info = 1; // reset interruption mask nb = write(uiofd, &info, sizeof(info)); if (nb != (ssize_t)sizeof(info)) { printf("Error restting interruption mask\n\r"); exit(EXIT_FAILURE); } // look for interrupt nb = read(uiofd, &info, sizeof(info)); if (nb == (ssize_t)sizeof(info)) { channel = *((unsigned *)(ptr + CHANNEL_OFFSET)); value = *((unsigned *)(ptr + VALUE_0_OFFSET)); threshold = *((unsigned *)(ptr + THRESHOLD_0_OFFSET)); fprintf(stderr, "Threshold exceeded on channel %d for %u times !\n\r", channel,info); fprintf(stderr, "Value is : %d\n\r", value); fprintf(stderr, "Threshold is set to : %d!\n\r", threshold); } } } else { // main process // recurrently get values from the PL while(CONTINU) { value = *((unsigned *)(ptr + VALUE_0_OFFSET)); t = time(NULL); tm = *localtime(&t); fprintf(fd, "\n\"%d-%d-%d %d:%d:%d\",\"%d\"", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, value); sleep(3); } } fclose(fd); munmap(ptr, MAP_SIZE); close(uiofd); printf("End of module 1 process\n\r"); exit(EXIT_SUCCESS); }
Ce code exécute 2 processus en parallèle. Le processus principale récupère la valeur retournée par le FPGA toutes les 3 secondes et l'écrit dans un fichier. Pour cela, le fichier du device driver est ouvert et la mémoire est mappée pour un obtenir un pointeur vers cette mémoire, ce pointeur permet ensuite d'accéder aux différents registres utilisés par le FPGA. Le sous processus permet de résoudre les interruptions envoyées par le FPGA. Puisque ces processus sont des boucles infinies, un handler est initialisé pour intercepter SIGTERM (kill) et SIGINT (Ctrl+C) afin de terminer le programme proprement.
Une fois petalinux-build et la Zybo bootée, on peut tester l'application :
module1 &
ou
module1 Ctrl+Z bg
>[1] 1303 >Start of module 1 process >Source set to BTNS >threshold set to 12
Les 4 switches de la Zybo modifient les 4 derniers bits du registre lu par le CPU, les leds s'allument en conséquence. Si l'on active les switches de manière à avoir une valeur supérieure à 12 :
>Threshold exceeded on channel 0 for 2 times ! >Value is : 13 >Threshold is set to : 12
On peut kill le processus :
kill 1303
ou
fg Ctrl+C
>End of module 1 process
On peut vérifier le contenu de ./channel0.csv :
"timestamp","value" "2019-2-4 13:23:35","0" "2019-2-4 13:23:38","2" "2019-2-4 13:23:50","13" "2019-2-4 13:23:53","15" "2019-2-4 13:23:56","0"
Module 2
Le module 2 consiste en une FFT afin d'obtenir la fréquence (voire la bande passante) et la puissance d'un signal. Toute la partie FFT et exploitation des résultats se fera dans le FPGA. Le CPU commandera la FFT.
L'IP FFT de Xilinx sera utilisé.
AXI & DMA
L'entrée et la sortie des données de la FFT se faisant en AXI-Stream, il faut utiliser la méthode DMA (Direct Memory Access) qui permettra de transformer une plage mémoire en AXI-stream.
L'IP AXI DMA de Xilinx sera utilisé.
Sources
- https://www.xilinx.com/support/documentation/ip_documentation/axi_dma/v7_1/pg021_axi_dma.pdf
- https://lauri.xn--vsandi-pxa.com/hdl/zynq/xilinx-dma.html
Vivado
On commence par créer un projet Vivado puis le block design suivant :
- Le Scatter Gather Engine de l'IP axi_dma est désactivé puisqu'il n'est pas utilisé.
- Le canal d'entrée (MM2S) est connecté au canal de sortie (S2MM) pour faire un test de transfert.
- L'IP axis_data_fifo permet une meilleur stabilité.
Puis générer le bitstream, exporter la partie hardware, et créer le FSBL.
Petalinux
Il faut ensuite créer un projet petalinux (comme dans cette partie).
Pour une meilleur gestion de la RAM avec la DMA, il faut changer la configuration du kernel :
petalinux-config -c kernel kernel > device driver > generic driver options > DMA Contiguous Memory Allocator
Créer l'application qui pilotera l'IP AXI DMA :
petalinux-create -t apps --template c --name axidma --enable
Editer project-spec/meta-user/recipes-apps/axidma/files/axidma.c :
#include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <termios.h> #include <sys/mman.h> #define MM2S_CONTROL_REGISTER 0x00 #define MM2S_STATUS_REGISTER 0x04 #define MM2S_START_ADDRESS 0x18 #define MM2S_LENGTH 0x28 #define S2MM_CONTROL_REGISTER 0x30 #define S2MM_STATUS_REGISTER 0x34 #define S2MM_DESTINATION_ADDRESS 0x48 #define S2MM_LENGTH 0x58 unsigned int dma_set(unsigned int* dma_virtual_address, int offset, unsigned int value) { dma_virtual_address[offset>>2] = value; } unsigned int dma_get(unsigned int* dma_virtual_address, int offset) { return dma_virtual_address[offset>>2]; } int dma_mm2s_sync(unsigned int* dma_virtual_address) { unsigned int mm2s_status = dma_get(dma_virtual_address, MM2S_STATUS_REGISTER); while(!(mm2s_status & 1<<12) || !(mm2s_status & 1<<1) ){ dma_s2mm_status(dma_virtual_address); dma_mm2s_status(dma_virtual_address); mm2s_status = dma_get(dma_virtual_address, MM2S_STATUS_REGISTER); } } int dma_s2mm_sync(unsigned int* dma_virtual_address) { unsigned int s2mm_status = dma_get(dma_virtual_address, S2MM_STATUS_REGISTER); while(!(s2mm_status & 1<<12) || !(s2mm_status & 1<<1)){ dma_s2mm_status(dma_virtual_address) ; dma_mm2s_status(dma_virtual_address); s2mm_status = dma_get(dma_virtual_address, S2MM_STATUS_REGISTER); } } void dma_s2mm_status(unsigned int* dma_virtual_address) { unsigned int status = dma_get(dma_virtual_address, S2MM_STATUS_REGISTER); printf("Stream to memory-mapped status (0x%08x@0x%02x):", status, S2MM_STATUS_REGISTER); if (status & 0x00000001) printf(" halted"); else printf(" running"); if (status & 0x00000002) printf(" idle"); if (status & 0x00000008) printf(" SGIncld"); if (status & 0x00000010) printf(" DMAIntErr"); if (status & 0x00000020) printf(" DMASlvErr"); if (status & 0x00000040) printf(" DMADecErr"); if (status & 0x00000100) printf(" SGIntErr"); if (status & 0x00000200) printf(" SGSlvErr"); if (status & 0x00000400) printf(" SGDecErr"); if (status & 0x00001000) printf(" IOC_Irq"); if (status & 0x00002000) printf(" Dly_Irq"); if (status & 0x00004000) printf(" Err_Irq"); printf("\n"); } void dma_mm2s_status(unsigned int* dma_virtual_address) { unsigned int status = dma_get(dma_virtual_address, MM2S_STATUS_REGISTER); printf("Memory-mapped to stream status (0x%08x@0x%02x):", status, MM2S_STATUS_REGISTER); if (status & 0x00000001) printf(" halted"); else printf(" running"); if (status & 0x00000002) printf(" idle"); if (status & 0x00000008) printf(" SGIncld"); if (status & 0x00000010) printf(" DMAIntErr"); if (status & 0x00000020) printf(" DMASlvErr"); if (status & 0x00000040) printf(" DMADecErr"); if (status & 0x00000100) printf(" SGIntErr"); if (status & 0x00000200) printf(" SGSlvErr"); if (status & 0x00000400) printf(" SGDecErr"); if (status & 0x00001000) printf(" IOC_Irq"); if (status & 0x00002000) printf(" Dly_Irq"); if (status & 0x00004000) printf(" Err_Irq"); printf("\n"); } void memdump(void* virtual_address, int byte_count) { char *p = virtual_address; int offset; for (offset = 0; offset < byte_count; offset++) { printf("%02x", p[offset]); if (offset % 4 == 3) { printf(" "); } } printf("\n"); } int main() { int dh = open("/dev/mem", O_RDWR | O_SYNC); // Open /dev/mem which represents the whole physical memory unsigned int* virtual_address = mmap(NULL, 65535, PROT_READ | PROT_WRITE, MAP_SHARED, dh, 0x40400000); // Memory map AXI Lite register block unsigned int* virtual_source_address = mmap(NULL, 65535, PROT_READ | PROT_WRITE, MAP_SHARED, dh, 0x0e000000); // Memory map source address unsigned int* virtual_destination_address = mmap(NULL, 65535, PROT_READ | PROT_WRITE, MAP_SHARED, dh, 0x0f000000); // Memory map destination address virtual_source_address[0]= 0x11223344; // Write random stuff to source block memset(virtual_destination_address, 0, 32); // Clear destination block printf("Source memory block: "); memdump(virtual_source_address, 32); printf("Destination memory block: "); memdump(virtual_destination_address, 32); printf("Resetting DMA\n"); dma_set(virtual_address, S2MM_CONTROL_REGISTER, 4); dma_set(virtual_address, MM2S_CONTROL_REGISTER, 4); dma_s2mm_status(virtual_address); dma_mm2s_status(virtual_address); printf("Halting DMA\n"); dma_set(virtual_address, S2MM_CONTROL_REGISTER, 0); dma_set(virtual_address, MM2S_CONTROL_REGISTER, 0); dma_s2mm_status(virtual_address); dma_mm2s_status(virtual_address); printf("Writing destination address\n"); dma_set(virtual_address, S2MM_DESTINATION_ADDRESS, 0x0f000000); dma_s2mm_status(virtual_address); printf("Writing source address...\n"); dma_set(virtual_address, MM2S_START_ADDRESS, 0x0e000000); dma_mm2s_status(virtual_address); printf("Starting S2MM channel with all interrupts masked...\n"); dma_set(virtual_address, S2MM_CONTROL_REGISTER, 0xf001); dma_s2mm_status(virtual_address); printf("Starting MM2S channel with all interrupts masked...\n"); dma_set(virtual_address, MM2S_CONTROL_REGISTER, 0xf001); dma_mm2s_status(virtual_address); printf("Writing S2MM transfer length...\n"); dma_set(virtual_address, S2MM_LENGTH, 32); dma_s2mm_status(virtual_address); printf("Writing MM2S transfer length...\n"); dma_set(virtual_address, MM2S_LENGTH, 32); dma_mm2s_status(virtual_address); printf("Waiting for MM2S synchronization...\n"); dma_mm2s_sync(virtual_address); printf("Waiting for S2MM sychronization...\n"); dma_s2mm_sync(virtual_address); // If this locks up make sure all memory ranges are assigned under Address Editor! dma_s2mm_status(virtual_address); dma_mm2s_status(virtual_address); printf("Destination memory block: "); memdump(virtual_destination_address, 32); }
Le déroulement du programme est le suivant :
- /dev/mem est ouvert pour obtenir les pointeurs
- 3 pointeurs sont créés pointant respectivement sur : le contrôleur AXI-DMA (l'interface S_AXI_LITE de l'IP axi_dma), la plage d'adresses source (qui contient les données à envoyer en stream), la plage d'adresse de destination (qui contiendra les données reçus)
- la plage mémoire source est initialisée avec des données, celle de destination est vidée
- la DMA est reset puis mise en pause en écrivant dans le contrôleur (les registres de contrôle dans lesquels il faut écrire sont détaillé dans la documentation)
- on indique la plage de mémoire qui servira d'entrée et celle qui servira de sortie à la DMA
- les canaux (d'entrée et de sortie) sont démarrés
- on indique la taille de données qui va transiter (il est important de le faire sur la canal de sortie en premier, sinon le canal d'entrée va commencer à envoyer les données alors que le canal de sortie n'est pas prêt)
- on attend que le transfert soit terminé : les deux canaux ont envoyé l'interruption (ici les interruptions sont masquées, mais il y a un registre qui permet de savoir si elles ont eu lieu)
- la plage mémoire de destination est affiché afin de voir si le transfert s'est bien déroulé
Après avoir boot la Zybo sur l'image petalinux précédemment créée, on peut tester le transfert :
axidma >Source memory block: 44332211 7dcddfdf 5a7fefa4 36aa3c9b ca2eea6a 5bf64f81 ebf7ffbb b7f710d2 >Destination memory block: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 >Resetting DMA >Stream to memory-mapped status (0x00000001@0x34): halted >Memory-mapped to stream status (0x00000001@0x04): halted >Halting DMA >Stream to memory-mapped status (0x00000001@0x34): halted >Memory-mapped to stream status (0x00000001@0x04): halted >Writing destination address >Stream to memory-mapped status (0x00000001@0x34): halted >Writing source address... >Memory-mapped to stream status (0x00000001@0x04): halted >Starting S2MM channel with all interrupts masked... >Stream to memory-mapped status (0x00000000@0x34): running >Starting MM2S channel with all interrupts masked... >Memory-mapped to stream status (0x00000000@0x04): running >Writing S2MM transfer length... >Stream to memory-mapped status (0x00000000@0x34): running >Writing MM2S transfer length... >Memory-mapped to stream status (0x00000000@0x04): running >Waiting for MM2S synchronization... >Waiting for S2MM sychronization... >Stream to memory-mapped status (0x00001002@0x34): running idle IOC_Irq >Memory-mapped to stream status (0x00001002@0x04): running idle IOC_Irq >Destination memory block: 44332211 7dcddfdf 5a7fefa4 36aa3c9b ca2eea6a 5bf64f81 ebf7ffbb b7f710d2
- N.B. Sans l'IP axis_fifo_data, les canaux restent bloqués en "halted DMAIntErr" ou "halted DMASlvErr".
Module FFT
Sources
Projet Vivado
On créer un projet Vivado avec le block design suivant :
- En entrée et en sortie de la FFT, des buffer ont été ajoutés pour plus de stabilité.
- Les IPs axi_gpio et edge_detect servent à configurer la FFT.
L'IP FFT est configuré comme suit :
- Transform length : 1024
- Target Clock : 100 MHz
- Architecture : Radix-4, Burst I/O (perd un peu en performance, mais la Zybo n'a pas assez de RAM pour Pipelined Streamming I/O)
- Data format : Floating point
- Output ordering : Natural order
Ce qui donne les bus suivant :
- N.B. SCALE_SCH est en fait inutile puisque en floating point l'option de scale est désactivée.
L'IP AXI DMA est configuré tel que :
- La taille des memory maps et des streams est de 64 bits pour correspondre aux bus de la FFT.
Enfin, les IPs axis_data_fifo sont configuré de la manière suivante :
- Encore une fois, la taille du bus est de 64 bits.
- La taille du buffer est de 1024, tout comme le nombre de points de la FFT. Ce n'est pas obligatoire, mais la stabilité est assurée.
Il faut ensuite compiler le bitstream, exporter la partie hardware, et générer le FSBL.
Données de test
Pour obtenir les données de test, un simple générateur de sinusoïde au format complexe suffit :
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <unistd.h> #define FREQUENCY 433000000 //433MHz #define PHASE 0 #define AMPLITUDE 2.3 #define SAMPLE_RATE 2000000000 //2GHz, must be at least > 2*FREQUENCY #define MAX_POINT 1024 void main() { double i; double t,re,im,x; double w = 2*M_PI*FREQUENCY; FILE* fd; fd = fopen("./sine_data","w"); for(i = 0 ; i < MAX_POINT ; i++) { t = (i / (SAMPLE_RATE)); x = (w*t) + PHASE; re = AMPLITUDE*cos(x); im = AMPLITUDE*sin(x); fprintf(fd,"%f, %f,\n", (float)re,(float)im); } fclose(fd); exit(EXIT_SUCCESS); }
Le code est compilé puis exécuté :
gcc -o sinegen -lm sinegen.c ./sinegen
- N.B. L'option -lm permet de lier la librairie math.h
Les données générées sont sauvegardées dans un fichier qui pourra ensuite être importé dans l'application qui commandera la FFT.
Petalinux
On peut maintenant reprendre le projet petalinux, et y créer deux nouvelles applications :
petalinux-create -t apps --template c --name fftx --enable petalinux-create -t apps --template c --name fftc --enable
- fftx servira à réaliser la FFT.
- fftc servira à configurer le module FFT (dans notre cas, on choisira juste FFT normal (et non inverse), étant déjà la configuration par défaut fftc est à titre d'exemple).
Le code de fftc, project-spec/meta-user/recipes-apps/fftx/files/fftc.c :
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define AXI_GPIO_UIOD "/dev/uio0" #define AXI_GPIO_ADDR 0x41200000 #define AXI_GPIO_MAP_SIZE 0x10000 #define AXI_GPIO_DATA_OFFSET 0x00 #define AXI_GPIO_TRI_OFFSET 0x04 //actually sch is disibale with floating point #define FFT_CONFIG 0x01 void main(){ int16_t fft_dir = FFT_CONFIG; int fd = open(AXI_GPIO_UIOD, O_RDWR); if(fd < 0) { printf("Error opening uio device\n"); exit(EXIT_FAILURE); } void* ptr = mmap(NULL, AXI_GPIO_MAP_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); *((unsigned *)(ptr + AXI_GPIO_TRI_OFFSET)) = 0; *((unsigned *)(ptr + AXI_GPIO_DATA_OFFSET)) = fft_dir; munmap(ptr, AXI_GPIO_MAP_SIZE); close(fd); printf("FFT config sent\n"); exit(EXIT_SUCCESS); }
- Ce programme écrit la direction dans le registre de contrôle de l'axi_gpio (0 étant pour l'écriture), puis écrit la configuration de la FFT dans le registre de données. L'axi_gpio pousse la donnée vers la FFT et edge_detect envoie un front montant dans l'entrée s_axi_config_tvalid pour valider la configuration.
Pour pouvoir utiliser fftc, il faut éditer project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi et y ajouter :
&axi_gpio_0 { compatible = "generic-uio"; };
Le code de fftx, project-spec/meta-user/recipes-apps/fftx/files/fftx.c :
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <termios.h> #include <sys/mman.h> #include <string.h> #include <stdint.h> #include "csine_data.h" #define AXI_GPIO_UIOD "/dev/uio0" #define AXI_GPIO_ADDR 0x41200000 #define AXI_GPIO_MAP_SIZE 0x10000 #define AXI_GPIO_DATA_OFFSET 0x00 #define AXI_GPIO_TRI_OFFSET 0x04 #define AXI_SLAVE_ADDR 0x40400000 //found in device tree or adress editor #define MM2S_BASE_ADDR 0x0e000000 #define S2MM_BASE_ADDR 0x0f000000 #define MEM_SIZE 0x10000 #define MM2S_CONTROL_REGISTER 0x00 //found in device tree #define MM2S_STATUS_REGISTER 0x04 #define MM2S_START_ADDRESS 0x18 #define MM2S_LENGTH 0x28 #define S2MM_CONTROL_REGISTER 0x30 //found in device tree #define S2MM_STATUS_REGISTER 0x34 #define S2MM_DESTINATION_ADDRESS 0x48 #define S2MM_LENGTH 0x58 typedef struct data_in_t { float data_re; float data_im; } data_in_t; typedef struct data_out_t { float data_re; float data_im; } data_out_t; //extern float sig_sine_waves[FFT_MAX_NUM_PTS * 2]; extern float sig_csine_waves[FFT_MAX_NUM_PTS * 2]; unsigned int dma_set(unsigned int* dma_virtual_address, int offset, unsigned int value) { dma_virtual_address[offset>>2] = value; } unsigned int dma_get(unsigned int* dma_virtual_address, int offset) { return dma_virtual_address[offset>>2]; } void dma_s2mm_status(unsigned int* dma_virtual_address) { unsigned int status = dma_get(dma_virtual_address, S2MM_STATUS_REGISTER); printf("Stream to memory-mapped status (0x%08x@0x%02x):", status, S2MM_STATUS_REGISTER); if (status & 0x00000001) printf(" halted"); else printf(" running"); if (status & 0x00000002) printf(" idle"); if (status & 0x00000008) printf(" SGIncld"); if (status & 0x00000010) printf(" DMAIntErr"); if (status & 0x00000020) printf(" DMASlvErr"); if (status & 0x00000040) printf(" DMADecErr"); if (status & 0x00000100) printf(" SGIntErr"); if (status & 0x00000200) printf(" SGSlvErr"); if (status & 0x00000400) printf(" SGDecErr"); if (status & 0x00001000) printf(" IOC_Irq"); if (status & 0x00002000) printf(" Dly_Irq"); if (status & 0x00004000) printf(" Err_Irq"); printf("\n"); } void dma_mm2s_status(unsigned int* dma_virtual_address) { unsigned int status = dma_get(dma_virtual_address, MM2S_STATUS_REGISTER); printf("Memory-mapped to stream status (0x%08x@0x%02x):", status, MM2S_STATUS_REGISTER); if (status & 0x00000001) printf(" halted"); else printf(" running"); if (status & 0x00000002) printf(" idle"); if (status & 0x00000008) printf(" SGIncld"); if (status & 0x00000010) printf(" DMAIntErr"); if (status & 0x00000020) printf(" DMASlvErr"); if (status & 0x00000040) printf(" DMADecErr"); if (status & 0x00000100) printf(" SGIntErr"); if (status & 0x00000200) printf(" SGSlvErr"); if (status & 0x00000400) printf(" SGDecErr"); if (status & 0x00001000) printf(" IOC_Irq"); if (status & 0x00002000) printf(" Dly_Irq"); if (status & 0x00004000) printf(" Err_Irq"); printf("\n"); } int dma_mm2s_sync(unsigned int* dma_virtual_address) { unsigned int mm2s_status = dma_get(dma_virtual_address, MM2S_STATUS_REGISTER); while(!(mm2s_status & 1<<12) || !(mm2s_status & 1<<1) ){ dma_s2mm_status(dma_virtual_address); dma_mm2s_status(dma_virtual_address); mm2s_status = dma_get(dma_virtual_address, MM2S_STATUS_REGISTER); } } int dma_s2mm_sync(unsigned int* dma_virtual_address) { unsigned int s2mm_status = dma_get(dma_virtual_address, S2MM_STATUS_REGISTER); while(!(s2mm_status & 1<<12) || !(s2mm_status & 1<<1)){ dma_s2mm_status(dma_virtual_address) ; dma_mm2s_status(dma_virtual_address); s2mm_status = dma_get(dma_virtual_address, S2MM_STATUS_REGISTER); } } void memdump(void* virtual_address, int byte_count) { char *p = virtual_address; int offset; for (offset = 0; offset < byte_count; offset++) { printf("%02x", p[offset]); if (offset % 4 == 3) { printf(" "); } } printf("\n"); } void main() { data_in_t input_buffer[FFT_MAX_NUM_PTS]; data_out_t output_buffer[FFT_MAX_NUM_PTS]; int j = 0; int dh = open("/dev/mem", O_RDWR | O_SYNC); // Open /dev/mem which represents the whole physical memory unsigned int* virtual_address = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, dh, AXI_SLAVE_ADDR); // Memory map AXI Lite register block unsigned int* virtual_source_address = mmap(NULL, sizeof(input_buffer), PROT_READ | PROT_WRITE, MAP_SHARED, dh, MM2S_BASE_ADDR); // Memory map source address unsigned int* virtual_destination_address = mmap(NULL, sizeof(output_buffer), PROT_READ | PROT_WRITE, MAP_SHARED, dh, S2MM_BASE_ADDR); // Memory map destination address memset(virtual_destination_address, 0, sizeof(output_buffer)); // Clear destination block for(int i=0 ; i < FFT_MAX_NUM_PTS ; i++) { // init input buffer input_buffer[i].data_re = sig_csine_waves[j]; j++; input_buffer[i].data_im = sig_csine_waves[j]; j++; } memcpy(virtual_source_address, input_buffer, sizeof(input_buffer)); // transfert data into source memory printf("Resetting DMA\n"); dma_set(virtual_address, S2MM_CONTROL_REGISTER, 4); dma_set(virtual_address, MM2S_CONTROL_REGISTER, 4); dma_s2mm_status(virtual_address); dma_mm2s_status(virtual_address); printf("Halting DMA\n"); dma_set(virtual_address, S2MM_CONTROL_REGISTER, 0); dma_set(virtual_address, MM2S_CONTROL_REGISTER, 0); dma_s2mm_status(virtual_address); dma_mm2s_status(virtual_address); printf("Writing destination address\n"); dma_set(virtual_address, S2MM_DESTINATION_ADDRESS, S2MM_BASE_ADDR); // Write destination address dma_s2mm_status(virtual_address); printf("Writing source address...\n"); dma_set(virtual_address, MM2S_START_ADDRESS, MM2S_BASE_ADDR); // Write source address dma_mm2s_status(virtual_address); printf("Starting S2MM channel with all interrupts masked...\n"); dma_set(virtual_address, S2MM_CONTROL_REGISTER, 0xf001); dma_s2mm_status(virtual_address); printf("Starting MM2S channel with all interrupts masked...\n"); dma_set(virtual_address, MM2S_CONTROL_REGISTER, 0xf001); dma_mm2s_status(virtual_address); printf("Writing S2MM transfer length...\n"); dma_set(virtual_address, S2MM_LENGTH, sizeof(output_buffer)); dma_s2mm_status(virtual_address); printf("Writing MM2S transfer length...\n"); dma_set(virtual_address, MM2S_LENGTH, sizeof(input_buffer)); dma_mm2s_status(virtual_address); printf("Waiting for MM2S synchronization...\n"); dma_mm2s_sync(virtual_address); printf("Waiting for S2MM sychronization...\n"); dma_s2mm_sync(virtual_address); // If this locks up make sure all memory ranges are assigned under Address Editor! dma_s2mm_status(virtual_address); dma_mm2s_status(virtual_address); memcpy((void*)output_buffer, virtual_destination_address, sizeof(output_buffer)); // tranfert data into output_buffer FILE* fd = fopen("./buffer","w"); for (int i = 0 ; i < FFT_MAX_NUM_PTS ; i++) { //save data into a file fprintf(fd, "%f,%f,\n", output_buffer[i].data_re,output_buffer[i].data_im); } fclose(fd); munmap(virtual_address,MEM_SIZE); munmap(virtual_destination_address,sizeof(data_out_t)*FFT_MAX_NUM_PTS); munmap(virtual_source_address,sizeof(data_in_t)*FFT_MAX_NUM_PTS); close(dh); printf("end \n\r"); exit(EXIT_SUCCESS); }
- Ce programme reprend le code de axidma.c pour réaliser le transfert axi stream. Il y a en plus deux structures de 1024 x 2 x 32 bits correspondant à la donnée qui va être envoyée à la FFT et celle qui sera reçue. Les données de la sinusoïde sont donc chargées dans la structure d'entrée qui est ensuite copiée dans la plage mémoire qui est envoyée en stream par l'axi_dma à la FFT. Les données reçus de la FFT sont copiées dans la structure de sortie puis enregistrées dans un fichier.
Résultats de la FFT
Pour utiliser les résultats de la FFT, nous allons encore une fois créer un IP custom. Cet IP permettra d'envoyer au CPU la bande passante du signal (à -3 dB) ainsi que la puissance maximale (en dBm).
Notes
- Pour déboguer le code de la partie FPGA, il est intéressant d'avoir un simulateur VHDL / verilog afin de ne pas avoir à recompiler le bitstream et reprogrammer le FPGA à chaque fois.
- Pour déboguer le code implémenté sur le matériel, il est mieux d'allouer plus de registres que nécessaire afin de stocker des valeurs intermédiaires et les vérifier.
Modification du block design
Maintenant, les résultats de la FFT ne sont plus envoyés vers le CPU mais seront envoyés vers un IP custom. Il faut donc supprimer le canal AXIS-S2MM de l'IP DMA. Le block design devient alors comme suit.
Calcul de la puissance
Les résultats étant en flottants, j'ai utilisé des IPs permettant de réaliser des calculs sur des flottant puis de les convertir en entiers pour les exploiter dans la logique. Le sous diagramme power_calc réalise le calcul de la puissance en suivant l'équation : P(dB) = 1000log((re² + im²) / points²). J'ai ajouter un facteur 100 pour garder la précision lors de la conversion en entier.
Le block design est le suivant :
Le stream est d'abord divisé en deux pour séparer la partie réelle et la partie imaginaire qui sont ensuite élevées au carré puis additionnées. Cet addition est alors divisée par le nombre de point au carré puis passé dans un logarithme népérien avant d'être converti en logarithme à base 10 et multiplié par 1000. Enfin la conversion en entier est faite.
On remarque les connections des différents t_valid pour synchroniser les calculs.
IP Custom
L'IP utilisé pour analysé les résultats dispose d'un bus d'entrée de 32 bits pour recevoir la puissance et d'une interface AXI slave pour que le CPU puisse récupérer les résultats. Il envoie d'ailleurs une interruption au CPU lorsque les résultats sont prêts.
L'IP dispose aussi d'un système de reset envoyé depuis le CPU qui permet de refaire un calcul.
Lorsque le bus de résultats arrive, les valeurs de puissance sont stockées dans un tableau (une comparaison simple est faite pour trouver l'indexe de puissance maximale). En parallèle la fréquence associée à la puissance est calculée avec l'équation F = index * Fe / N (où Fe est la fréquence d’échantillonnage et N le nombre de points).
Une fois que tous les résultats sont obtenus, la logique part de l'indexe de puissance pour effectuer une comparaison sur les indexes supérieurs et inférieurs dans le but de trouver la bande passante à -3 db.
Enfin l'interruption est envoyée et les fréquences (haute et basse) de la bande passante sont stockées dans des registres, ainsi que la puissance maximale.
Module 3
Ce module FPGA est constitué de 3 IPs :
- Un générateur de trafic TCP/IP UDP/IP utilisé pour les tests
- Un parser permettant de récupérer des informations dans les paquets
- Un compteur qui vérifie le nombre de paquets reçus (adresse et port de destination) afin d'envoyer des avertissements.
IP gérant du traffic TCP/IP ou UDP/IP
Cet IP génère du trafic ethernet IPv4 TCP ou UDP utilisé pour les tests. Pour générer le trafic j’utilise une sorte de machine à état avec un curseur qui boucle sur les différents headers ethernet, IP, TCP, ou UDP. Le trafic est synchroniser entre le générateur et le receveur grâce au préambule du header ethernet, qui doit normalement être utilisé pour synchroniser les horloges. La plupart des headers ont une valeur par défaut, seulement les adresses IP, les ports et le protocol peuvent être choisi depuis le CPU grâce à une application C qui permet de choisir le type de trafic, par exemple :
$ ip-order -x -p TCP -s 192.168.10.51:22 -d 192.168.10.50:22
Permet de générer du trafic TCP, tandis que :
$ ip-order -k
Stop le trafic.
Le générateur est tout de même capable de préciser la taille des paquets avec les headers IPv4 internet header length (qui définit la taille du header IPv4) et total length (qui définit la taille du paquet IPv4), ainsi que le header TCP data offset (qui définit la taille du header TCP) ou le header UDP length (qui définit la taille du paquet UDP, sachant que le header fait toujours 8 octets).
IP analysant les trames
Le parser reçoit le trafic et l’analyse. Il enregistre notamment les adresses IP, les ports et le protocol, qui sont ensuite envoyés au compteur pour contrôler le nombres de requêtes. En parallèle, il transfère les paquets à un destinataire. Comme avec le premier IP, j’utilise un curseur pour parcourir les headers. La taille des paquets est obtenus grâces aux headers pour ne pas faire d’erreur de parsing. Durant chaque transfert de trame, dès lors que l’IP connaît l'adresse IP de destination, il vérifie si cette adresse est désignée (par le troisième IP) comme subissant une attaque ; si c’est le cas, le transfert du paquet est stoppé et le curseur de la machine à état retourne à l’état initial. Il en est de même lorsque le port de destination est connu.
IP émettant les alertes
Le compteur analyse les informations reçues par le parser et bloque le trafic en fonction de ces informations et des ordres du CPU. La vérification se fait sur une fenêtre de dix secondes grâce à un compteur. Chaque adresse IP et port protégé dispose de son propre compteur. A chaque fois qu’un paquet a été analysé, le compteur reçoit l’adresse et le port de destination du paquet de la part du parser. Il vérifie alors si l’un des deux est inclu dans ceux qu’il protège, si c’est le cas le compteur associé est incrémenté. Lorsqu’un compteur associé à une adresse ou un port protégé dépasse le seuil fixé par le CPU, une interruption est envoyé et le parser est informé. A l’expiration de la fenêtre de dix secondes, tous les compteurs sont réinitialisés. Pour configurer le compteur, j’utilise une application C qui permet de définir le seuil (en paquets par seconde sur une fenêtre de dix secondes) à partir duquel une adresse ou un port est bloqué, par exemple :
$ ip-config -t 10000
Fixe le seuil à 10 000 paquets par seconde.
La même application est utilisée pour configurer les adresses ou ports protégés :
$ ip-config -i 0 -a 192.168.10.50 $ ip-config -i 1 -p 22
Configure respectivement l’adresse 192.168.10.50 en tant que première adresse protégée et le port 22 en tant que deuxième port protégé.
Enfin, une dernière application permet d’intercepter les interruptions du compteur et d’afficher l’adresse ou le port bloqué dans le terminal.
Block desgin final
On remarque (en vert) le bus permettant de faire circuler le trafic entre le générateur et le parser. Et en rouge, l'interruption envoyé par le compteur au CPU. Les liens entre le parser et le compteur permettent d'échanger des informations pour bloquer les adresses ip ou les ports.