51 4 3MB
DSP C6000
BAHTAT Mounir
DSP C6000 Architecture & programmation C/ASM
Rédigé par : Mounir BAHTAT Type de cours : TP guidé Catégorie du cours : Systèmes embarqués & Temps réel Pré-requis : Mise à jour le : 18-12-2012
Easy Learn www.easylearn.max.st
0
DSP C6000
BAHTAT Mounir
Sommaire Partie 1 : Architecture & Programmation C embarqué Chapitre 1 : DSP et architecture Chapitre 2 : Notre premier programme en C embarqué Chapitre 3 : Techniques d’optimisation en C
Partie 2 : Programmation ASM C66x Chapitre 1 : Notre premier code assembleur Chapitre 2 : Ecrire un code assembleur optimisé
1
DSP C6000
BAHTAT Mounir
Introduction Une unité centrale de traitement (CPU : Central Processing Unit) est l’élément jouant le rôle d’un cerveau dans tout système. Cet élément ne permet pourtant que d’effectuer des opérations arithmétiques et logiques basiques, ainsi que des opérations d’E/S [Entrée/Sortie]. Depuis 1970, les CPU (plus précisément les microprocesseurs) ne cessent de s’optimiser. Le besoin en temps réel dans des applications liées aux traitements de signal, a engendré l’apparition d’un nouveau type de processeurs optimisés, qui portent l’acronyme de DSP (Digital Signal Processor).
Les DSP et les systèmes embarqués temps réel Un système embarqué est un système électronique [matériel] et informatique [logiciel] autonome, qui doit souvent prendre en compte des contraintes temps réel. Les caractéristiques essentielles qui sont généralement exigées pour un système embarqué sont : -
Le coût doit être le plus faible possible Consommation énergétique la plus faible possible, dû à l’utilisation des batteries [en général] Encombrement le plus réduit possible Performance taillée spécifiquement pour une certaine application [puissance de calcul, …]
L’architecture d’un système embarqué est constituée principalement/généralement d’un élément de traitement (qui peut être GPP [General Purpose Processor] et/ou DSP [Digital Signal Processor] et/ou SoC [System On Chip] basé sur FPGA/ASIC), des mémoires et des interfaces de communication avec des périphériques [comme exemple : écran tactile, GPS, …]. La partie logicielle qui sera présente dans des mémoires Flash pour pouvoir être exécutée par un microprocesseur, est appelée : "firmware". Ci-après un exemple de système embarqué :
Gumstix Overo COM [Computer On Module], avec Wifi et Bluetooth
Partie 1 : Architecture & Programmation C embarqué Chapitre 1 : DSP et architecture Les DSP sont apparus vers 1978, pour répondre aux attentes temps réel des algorithmes de traitement de signal. Ils se sont caractérisé principalement des processeurs ordinaires par l’opération MAC (Multiplication & Accumulation) en un seul cycle d’horloge, alors que cette dernière opération est couteuse en termes de cycles sur d’autres types de microprocesseurs. Les DSP se dotent également d’une architecture Harvard modifié, qui contrairement aux architectures Von Neumann, permet l’accès simultané au programme et aux données, via des bus dédiés. L’utilisation de ce type de processeur dans un cadre de traitement de signal nécessitera des interfaces de conversion analogique/numérique (échantillonnage à une certaine fréquence), du fait que le DSP ne pourra traiter les données qu’aux cycles d’horloge, comme le montre la figure suivante :
2
DSP C6000
BAHTAT Mounir
ADC=[Analog-Decimal Converter] ; DAC=[Decimal-Analog Converter] Texas Instruments (TI) occupe 70% du marché des DSP, laissant 30% aux autres concurrents (Motorola, Analog Devices, Lucent Technologies, …)
On s’intéressera durant ce TP guidé, à un DSP propre au Texas Instruments, ces DSP se trouvent catégorisés en 3 : C2000 ; C5000 et C6000. Les caractéristiques/applications de chacune des familles sont citées ci-dessous :
On s’intéressera particulièrement aux DSP performants C6000, qui se trouvent encore catégorisés en 2 : à virgule fixe et à virgule flottante. Les DSP à virgule flottante peuvent effectuer des opérations à virgule (nombres réels) en 1 cycle, alors que ça doit prendre plusieurs cycles sur un DSP à virgule fixe (spécifique aux nombres entiers) ; Ceci est dû à la présence des blocs matériels dans l’architecture du DSP qui sont spécialisés dans les opérations flottantes, alors que pour un DSP à virgule fixe une opération flottante est traduite en une combinaison de plusieurs opérations fixes [entières], engendrant une augmentation considérable des cycles lors de l’exécution. Les DSP de TI de la famille C6000 classés en performance sont listés ci-dessous :
3
DSP C6000
BAHTAT Mounir
DSP à virgule fixe
DSP à virgule flottante
4
DSP C6000
BAHTAT Mounir
Le DSP qui sera utilisé dans ce TP est le tout dernier TMS320C6678 à virgule fixe ET flottante. Ce DSP qui se présente sous la forme d’un circuit intégré est fournie dans une carte de développement avec d’autres périphériques et mémoires afin faciliter son test et utilisation. La carte se présente ainsi :
On y trouve principalement le DSP, des interfaces pour les protocoles de communication haut-débit (Ethernet, HyperLink, AMC), de la mémoire dynamique de 512 Mo, un émulateur XSD100v1 qui a pour rôle de faire communiquer un PC avec le DSP pour des objectifs de débogage : chargement du code logiciel vers les mémoires pour exécution par les cœurs du DSP, mise en pause/marche de chacun des cœurs, accès direct aux mémoires, … 18 interrupteurs DIP-Switch permettent la configuration statique de la carte (horloge, protocole PCIe, …). Un émulateur externe à haute vitesse peut être connecté via 60-pin afin d’effectuer des opérations de débogage rapide. Le connecteur AMC dans l’image ci-dessus transporte des lignes des protocoles de communication haut-débit tel que RapidIO ou PCIe (débits jusqu’à 5 Gbps [giga bit per second]). Finalement des boutons poussoirs pour "reset" sont également disponibles, l’expression "warm reset" exprime une réinitialisation sans mettre hors tension des composants. La configuration usine [par défaut] des DIP-Switch est la suivante :
Cette configuration statique initiale choisit : -
Un fonctionnement en "Little Endian" ; la différence entre "Little Endian" et "Big Endian" existe au niveau de la façon d’adressage d’un octet au sein d’un mot (de 32-bit) comme le montre la figure suivante :
5
DSP C6000
-
BAHTAT Mounir
"I2C Boot Master Mode" -> lire après la mise sous tension, du code programme à partir d’une mémoire EEPROM de 128Ko via le protocole série I2C Module PCIe désactivé
L’architecture interne du DSP C6678 est la suivante :
Le DSP contient 8 cœurs chacun pouvant se comporter comme un processeur indépendant, fonctionnant jusqu’à 1.25 GHz. Plusieurs niveaux de mémoires sont présents : -
L1 local pour chacun des cœurs, divisé en 2, L1D (pour les données) et L1P (pour le programme) de taille 32Ko chacun ; c’est le niveau le plus proche au cœur, résultant en un accès le plus rapide possible, sans latences (débit de 16 octets par cycle)
6
DSP C6000 -
BAHTAT Mounir
L2 local pour chacun des cœurs de taille 512 Ko ; l’accès à cette mémoire est moins rapide qu’en L1
L1 ou L2 peuvent être aussi utilisé en mode CACHE, durant ce mode, on sauve l’accès aux données lointaines (à partir de la mémoire dynamique DDR3 externe par exemple) en les chargeant au L1 ou L2 d’avance. -
MSM (Multicore Shared Memory) est une mémoire statique SRAM de 4 Mo commune à tous les cœurs Un contrôleur de la mémoire dynamique DDR3 (Double Data Rate), il s’occupe des différentes opérations de contrôle pour la mémoire externe DDR3 (rafraîchissement périodique, READ, WRITE, …) ; L’accès à ce type de mémoire est relativement lent par rapport aux mémoires statiques.
Ci-après une brève description des autres modules de l’architecture : -
-
-
Le périphérique « Debug and Trace » s’occupera des opérations de débogage entre DSP et PC La mémoire « Boot ROM » est une mémoire non volatile, de taille 128 Ko, servant pour sauvegarder le code programme, même après mise hors tension des périphériques du DSP Le bloc « Semaphore » protège l’accès concurrent aux périphériques par plusieurs maîtres (particulièrement par les 8 cœurs) Le PLL (Phase-Locked Loop) permettra la génération des signaux aux fréquences souhaitées L’EDMA (Enhanced Direct Memory Access) permet le transfert mémoire sans l’intervention du CPU, ceci apporte une importante majeure pour les applications nécessitant l’accès à une quantité importante de la mémoire Le bloc EMIF16 est un contrôleur d’une mémoire ROM externe Plusieurs protocoles de communication haut-débit sont disponibles également sur le DSP : PCIe (5 Gbps), RapidIO (5 Gbps), ETHERNET (1 Gbps), TSIP (32 Mbps), HyperLink (50 Gbps, entre 2 DSP C6678), I2C, UART, SPI Finalement, le « Multicore Navigator » a pour rôle d’optimiser le transfert des paquets issus des interfaces de communications haut-débit sur le bus TeraNet, ainsi que de réduire les latences des transferts mémoires entre cœurs
Chacun des cœurs C66x CorePac est présenté sous le schéma suivant :
7
DSP C6000
BAHTAT Mounir
On trouve par banc : -
32 registres de 32-bit nommés A0 -> A31 / B0 -> B31 4 unités .L / .S / .M / .D o .L : Unité arithmétique et logique, capable d’exécuter des instructions arithmétiques (additions, soustractions) et logiques (AND, OR, …), chacune des unités est capable de faire au maximum 2 additions/soustractions 32-bit flottantes/fixes par cycle ; Notez bien que toutes les instructions agissent seulement sur les 64 registres disponibles par cœur, et n’admettent pas des opérandes liées à la mémoire externe. o .S : Unité de branchement et de décalage, capable d’exécuter des opérations de branchement [saut du flux d’exécution d’un programme à une position bien déterminée] ou de décalage des registres. Cette unité est capable également d’exécuter des opérations d’additions/soustractions flottante ou fixe (au maximum 2 opérations par unité) o .M : Unité de multiplication, capable d’exécuter au maximum 4 multiplications 32-bit flottantes/fixes o .D : Unité de chargement et de stockage, capable de charger/stocker une donnée sur 64-bit
Ainsi, le cœur est capable d’exécuter 8 instructions différentes en parallèle par cycle (une instruction par unité). Le cœur est également capable d’exécuter 8 multiplications 32-bit flottante/fixe par cycle [en utilisant les deux unités de multiplication], et 8 additions/soustractions 32-bit flottante/fixe par cycle [en exploitant les 4 unités .L1/.L2/.S1/.S2] Ce DSP multi-cœur [à une technologie de 40nm], vu sa puissance de calcul énorme, se dresse comme le meilleur processeur existant pouvant satisfaire une exigence en temps réel avec la moindre consommation en puissance, en effet suivant les Benchmarks certifiés du BDTI, ce DSP dépasse en performance l’ensemble des plateformes industrielles similaires :
Le TMS320 C6678 est le premier DSP à 10 GHz. A une puissance de calcul de 320 GMACS en virgule fixe et 160 GFLOPS en virgule flottante à simple précision, 40 GFLOPS en virgule flottante à double précision. Avec une consommation moyenne de 10W seulement. Offrant alors une efficacité maximum de 4 GFLOPS/W [DP].
8
DSP C6000
BAHTAT Mounir
L’architecture d’un cœur de ce DSP est de type VLIW (Very Long Instruction Word), qui dispose de plusieurs unités fonctionnelles capables d’exécuter plusieurs instructions hétérogènes en 1 cycle d’horloge [ILP, Instruction Level Parallelism]. Dans notre cas il s’agit d’exécuter 8 instructions différentes en parallèle à la fois, ceci est possible grâce à un bus programme sur 256-bit [emportant 8 instructions de 32-bit chacune], comme le montre la figure des bus suivante :
L’architecture du VLIW est plus simple qu’un CISC/RISC, du fait que le parallélisme des instructions n’est pas spécifié en matériel mais laissé à la partie logicielle [tâche du programmeur ASM ou du compilateur C]. L’inconvénient que peut présenter ce nouveau type d’architecture est la taille du code source, en effet, dans le cas où on dispose de moins que 8 instructions à exécuter en parallèle il faudra utiliser des instructions NOP pour marquer la nonutilisation de quelques unités fonctionnelles ; par conséquence, de la mémoire programme doit être réservée pour chaque instruction NOP utilisée. Si par exemple en moyenne on ne disposait que de 4 instructions exécutées par cycles, alors la moitié du code source serait des NOP ! Une nouvelle amélioration par rapport au VLIW standard a été implémentée par TI sur C66x portant le nom de "VelociTI", réduisant de manière considérable le phénomène de la taille du code source :
La figure suivante illustre le chemin des données du banc B dans un cœur c66x [taille par défaut d’un bus est 64-bit] :
9
DSP C6000
BAHTAT Mounir
On peut voir alors que les unités .L et .S peuvent avoir 2 opérandes de 64-bit chacun [src1/src2] et un port de sortie [dst] de 64-bit également. L’unité .M dispose d’un port de sortie de 128-bit [dst1/dst2]. 2 chemins entre les 2 bancs sont disponibles [1X et 2X], appelés "cross path". Le chemin 1X transporte 64-bit des données du banc B vers le banc A, alors que le chemin 2X transporte 64-bit du banc A vers le banc B. Finalement, on conclue que la communication entre les bancs est limitée à 64-bit des données dans les deux sens par cycle.
Chapitre 2 : Notre premier programme en C embarqué L’outil de débogage qui va nous permettre d’écrire nos codes C ou ASM, leurs compilations ainsi que leurs chargements sur le DSP cible est : Code Composer Studio v5.1 de Texas Instruments
10
DSP C6000
BAHTAT Mounir
Il s’agit d’un outil puissant et gratuit, capable de faire l’émulation (quand on dispose d’une carte de développement pour DSP) ou la simulation (quand on n’a pas de DSP, le simulateur donne pratiquement des résultats similaires au cas réel) L’outil est téléchargeable à partir du lien suivant : http://processors.wiki.ti.com/index.php/Download_CCS (version 5.1.0 de taille de 1200 Mo) Un nouveau projet peut se créer à partir du : File -> New -> CCS Project
Spécifier un nom pour votre projet, le type du projet doit être exécutable (pas une librairie de fonctions) ; puisque notre DSP d’intérêt est C6678, il faut spécifier la famille C6000, de type générique C66xx. Valider afin de créer un nouveau projet vide. Il faut ensuite créer une configuration de la cible souhaitée (Target Configuration), qui va nous permettre de choisir la référence exacte de notre DSP. Pour cela, cliquer sur « New Target Configuration File » de la fenêtre « Target Configurations » (faites View -> Target Configurations si nous ne voyez pas la fenêtre en question) Une invite vous demande de spécifier le nom du fichier de configuration .ccxml, après validation, choisissez la cible souhaitée :
11
DSP C6000
BAHTAT Mounir
Dans le cas de l’émulation, il faut sélectionner tout d’abord le type de la connexion à « Texas Instruments XDS100v1 USB Emulator », puis sélectionner TMS320C6678. Dans le cas de la simulation, on choisira « C6678 Device Cycle Approximate Simulator, Little Endian » Il faut lier après, la configuration crée, avec votre projet CCS. Pour cela cliquez-droit votre fichier de configuration dans la fenêtre « Target Configurations » et ensuite Link File To Project -> Nom de votre projet :
Une fois fait, le fichier sera ajouté et sera visible parmi les fichiers de votre projet. Une autre information importante à fournir au compilateur c’est dans quel niveau mémoire (L1, L2, MSM, DDR3, ROM) il faut charger le programme qu’on écrit. Cette information est à spécifier dans un fichier .cmd qui a cette structure minimale : -stack
Taille de la pile en octets
-heap
Taille d’allocations dynamiques en octets
MEMORY { }
Liste des mémoires disponibles sur le DSP cible
SECTIONS { }
Le programme est découpé en sections pouvant être mappées à plusieurs mémoires
La liste des mémoires disponibles sur TMS320C6678 est la suivante :
Plusieurs sections sont prédéfinies pour un programme écrit en langage C ; quelques sections importantes sont décrites ci-dessous :
12
DSP C6000
BAHTAT Mounir
Un exemple de fichier de configuration .cmd est le suivant : -stack 0x5000 -heap 0x5000 MEMORY { L2SRAM : o = 0x00800000 , l = 0x00080000 } SECTIONS { .text > L2SRAM .data > L2SRAM .cinit > L2SRAM .const > L2SRAM .cio > L2SRAM .far > L2SRAM .near > L2SRAM .fardata > L2SRAM .sysmem > L2SRAM .stack > L2SRAM }
Dans la partie MEMORY les mémoires utilisées de la cible sont listées, avec leurs adresses d’origine (o) et taille (l) Dans la partie SECTIONS on affecte chacune des sections définies vers l’identifiant d’une mémoire Les valeurs saisies dans le fichier sont en format hexadécimal. On sauvegarde le fichier sous l’extension .cmd et on le place dans le même répertoire de notre projet [qui doit se trouver dans le workspace] Enfin, on se propose d’exécuter le programme de test suivant : #include void main(void) { printf("c66x test end\n"); }
Faites Project -> Build Project, puis Run -> Debug
13
DSP C6000
BAHTAT Mounir
Le résultat de la compilation, est la génération d’un fichier binaire .out (traduction binaire d’un code assembleur) qui sera chargé par la suite dans une mémoire. Une fenêtre vous demandera de cocher les cœurs qu’on veut utiliser. Après validation, le programme se chargera dans les mémoires spécifiées et vous devez être capable de voir tous les cœurs dans la fenêtre debug, prêts pour exécuter le programme :
Après exécution sur un cœur, vous devez voir dans la console le résultat de l’instruction printf : [TMS320C66x_0] c66x test end
Chapitre 3 : Techniques d’optimisation en C Durant ce chapitre, on passera à travers les techniques couramment utilisées en C sur un cœur C66x, ceci, en essayant d’implémenter et d’optimiser comme application, un algorithme de produit matriciel complexe. Le produit matriciel est un algorithme essentiel dans un nombre de blocs de traitement du signal ainsi que d’autres applications, dont on cite comme exemple : DFT (Discrete Fourrier Transform), BF (Beam Forming), inversion des matrices, résolution des systèmes linéaires, recherche de déterminant, LQR (commande automatique optimale), … On considère des matrices carrés de taille N, et que chacun des éléments des matrices est un nombre complexe (ayant une partie réelle et une partie imaginaire)
Le produit matriciel des matrices A et B donnera une matrice carré C de taille N, vérifiant :
14
DSP C6000
BAHTAT Mounir
On aura tout d’abord besoin d’un type des données pouvant stocker un nombre complexe (partie imaginaire & partie réelle). La solution la plus facile étant de construire notre propre type décrit par une structure ayant deux champs : réel et imaginaire [NB : le type float est un type supportant la virgule flottante, codé sur 4 octets] : typedef struct { float re; float im; } complex;
Les matrices seront représentées alors par des tableaux bidimensionnels, ainsi : #define N 12 complex inpA[N][N]; complex inpB[N][N]; complex outp[N][N];
On choisit alors que l’élément A(i,j) soit équivalent à la notation A[i][j] (i pour la ligne, j pour la colonne) :
Il est intéressant de savoir également comment les données sont organisées en mémoire, et savoir dans quelle adresse est stocké chacun des éléments de la matrice. On peut représenter la mémoire comme une liste d’adresses unidimensionnelle, de la façon suivante :
Il est à noter que l’ordre des champs dans la structure affecte la façon avec laquelle les éléments sont placés dans la mémoire. De manière générale la notation bidimensionnelle [i][j] peut se traduire à la notation unidimensionnelle
15
DSP C6000
BAHTAT Mounir
[i*N+j]. Notez bien que inpA (l’identifiant de notre tableau bidimensionnel) en C n’est qu’une adresse pointant vers le premier élément de notre tableau, la notation inpA+1 incrémente le pointeur par une quantité égale à la taille du type des données sur lequel inpA pointe (pour notre cas il s’agit du type complex, de taille 8 octets) On se propose ensuite d’écrire une fonction de multiplication matricielle ayant le prototype suivant : void produit_matriciel(complex inpA[N][N], complex inpB[N][N], complex outp[N][N], unsigned int n);
On fournira à cette fonction, 3 pointeurs vers les 3 matrices concernées, ainsi que la taille utilisée. Deux opérations sont nécessaires afin de faire un produit matriciel : somme complexe & multiplication complexe :
On écrira alors les 2 fonctions C suivantes : complex somme(complex argA, complex argB) { complex res; res.re=argA.re+argB.re; res.im=argA.im+argB.im; return res; } complex produit(complex argA, complex argB) { complex res; res.re=argA.re*argB.re-argA.im*argB.im; res.im=argA.re*argB.im+argB.re*argA.im; return res; }
En appliquant la formule bien connue du produit matricielle, la fonction de multiplication complexe s’écrira : void produit_matriciel(complex inpA[N][N], complex inpB[N][N], complex outp[N][N], unsigned int n) { int i,j,k; for (i=0;i Clock -> Reset Pour supprimer les breakpoints : Run -> Remove All Breakpoints De même on mesure d’autres événements importants : -
Nombre des cycles CPU (CPU.cycle) : 2709 cycles Latences mémoire (CPU.stall.mem.L1D) : 268 cycles Nombre total des cycles (cycle.Total) : 2984 cycles
Note : Durant tout ce document les résultats reportés sont ceux du simulateur (en émulation, il est prévu qu’il y en ait une petite différence) A ce stade, on se demande pourquoi on n’est pas capable d’atteindre une efficacité aussi importante (à peine 30% actuellement). Afin de répondre à cette question, on essayera d’évaluer en un niveau plus bas les instructions pouvant être réalisé au niveau du cœur C66x, vu les ressources disponibles, afin d’implémenter notre produit matriciel. La toute première opération qu’on demande de faire dans notre produit matriciel est de charger deux nombres complexes de chacune des deux matrices, faire leur produit matriciel, et accumuler le résultat :
Les unités .D1 et .D2 peuvent charger chacune 64-bit des données, donc chacune des unités chargera un nombre complexe par cycle. Une unité .M1 de multiplication peut faire en 1 cycle les 4 multiplications 32-bit requises dans une multiplication complexe sur les données chargées. Les unités .L1 et .S1 peuvent faire les 4 accumulations 32-bit restantes.
La séquence possible des instructions dans le temps (par cycle) est schématisée ci-dessous :
27
DSP C6000
BAHTAT Mounir
Au premier cycle, on utilise .D1 et .D2 afin de charger des données, qui seront disponibles au cycle suivant, on utilisera le .M1 afin de multiplier les données chargée, au cycle qui suit les résultats de multiplications seront prêts à être additionnées par .L1 en premier lieu afin de finaliser le calcul du produit complexe, et puis accumulés par .S1 dans le cycle suivant. Il s’agit de la séquence idéale des instructions qu’on peut avoir, dans ce cas, on est capable de faire 4 MAC par cycle seulement, encore loin de la capacité de calcul maximale d’un cœur C66x : 8 MAC par cycle (par rapport auquel on calcule l’efficacité de notre implémentation C). Ceci explique l’efficacité réduite de notre code ( L2SRAM .fardata > L2SRAM .sysmem > L2SRAM .stack > L2SRAM .mysect > MSMCSRAM }
32
DSP C6000
BAHTAT Mounir
Partie 2 : Programmation ASM C66x Chapitre 1 : Notre premier code assembleur Texas Instruments propose 3 manières pour écrire un programme pour c66x : langage c/c++ ; assembleur linéaire ; assembleur standard c66x :
Comme montré ci-dessus, la programmation c/c++ masque la complexité bas niveau du cœur c66x, à savoir : [le choix des instructions assembleurs appropriées, choix des registres/répartition de l’exécution sur les 2 bancs (A et B), mapping/ordonnancement des instructions sur les différentes unités, pipeline], mais en contrepartie, une performance maximale n’est pas garantie. Aussi, le comportement du même code c/c++ peut varier suivant chaque mise à jour/version du compilateur [problème de portabilité]. Une nouvelle manière de programmation ASM est l’assembleur linéaire, qui donne la possibilité d’imposer le choix des instructions assembleur à être utilisé, ainsi que la manière d’allocation des registres. Par contre les autres détails sont gérés automatiquement par le compilateur [assembly optimizer]. Ce mode de programmation est plus facile que l’assembleur standard, et procure généralement de meilleurs performances par rapport au langage c/c++. L’assembleur standard c66x gère manuellement en bas niveau les opérations sur le cœur c66x et choisit le parallélisme convenable des instructions [ILP]. Ce mode de programmation peut offrir le maximum de performance possible. Aussi, le code écrit sera exécuté tel quel par le cœur, donc offre également le maximum de portabilité, puisque ce code ne sera pas modifié par le compilateur. On s’intéressera par la suite à ce mode de programmation. Le flux de génération du code c66x est schématisé ainsi :
33
DSP C6000
BAHTAT Mounir
L’extension .sa est celle de l’assembleur linéaire, on écrira alors directement du code assembleur standard en .asm indépendamment de tout outil de post-optimisation. Le code binaire qui sera chargé dans la mémoire pour être exécuté a l’extension .out Il est noté que l’assembleur est plus difficile à construire qu’un code C, en compromis aux performances. Alors qu’en général on souhaite seulement optimiser des routines critiques principaux, et que d’autres routines n’ont pas intérêt à être optimiser tellement (routines d’initialisations, déclarations des données, allocations, …). On se propose par la suite de mixer l’utilisation du C et de l’ASM standard ; le langage C pour des routines/opérations n’ayant pas besoin d’être optimisées, et l’ASM pour les fonctions principaux et critiques en temps d’exécution. La structure minimale d’un code assembleur est la suivante (routine appelée _myasm) : .global _myasm _myasm: ; Instructions B .S2 B3 NOP 5 _myasm: est une étiquette indiquant le début de la définition de la routine ASM. La directive .global affecte une
visibilité globale à l’entité "_myasm", de telle façon que ça soit possible de l’appeler à partir d’un code C. Une instruction assembleur a la syntaxe suivante : (nom d’instruction) (.unité) (opérandes). Ainsi l’instruction B (branchement) s’exécutera sur l’unité .S2 avec l’opérande dans le registre B3. Un branchement va changer le flux d’exécution d’un code vers une autre zone du mémoire programme, dans notre cas il s’agit de se brancher aux instructions qui se trouvent à l’adresse programme stockée dans B3. Cette valeur [adresse] contenue dans B3 est celle des instructions de la fonction mère qui doivent être exécutées juste après la fin de notre routine assembleur. Ainsi, il est impératif de se brancher à cette adresse à la fin de notre routine, afin de continuer le flux normal d’exécution de notre programme principale. Notez que l’adresse convenable est mise en B3 lors de l’appel à la fonction assembleur, il faut ainsi sauvegarder sa valeur pour pouvoir s’y brancher en fin d’instructions.
34
DSP C6000
BAHTAT Mounir
On remarque également l’utilisation de NOP 5 [5 cycles sans opérations], juste après l’instruction de branchement. En effet, le branchement ne sera effectivement exécuté qu’à 6 cycles après son lancement, ainsi, l’attente de 5 cycles est exigée. Ceci est le cas pour toutes les instructions assembleur c66x, nécessitant une latence, appelée "Delay Slots" [DS], avant de pouvoir retourner un résultat/exécution. Par exemple pour le cas d’un branchement on trouve un "delay slots"=5, pour une multiplication flottante à simple précision la latence DS=3. Alors si une multiplication va durer 4 cycles pour retourner du résultat, pourquoi dit-on qu’on peut faire avec ce DSP 8 multiplications par cycle ? Une autre caractéristique des instructions assembleurs c66x est la latence sur les unités d’exécution "Functional Unit Latency" [FUL], qui indique le nombre des cycles que l’instruction assembleur doit occuper sur une unité d’exécution. Presque seules les instructions à double précision nécessitent FUL>1, par contre, les autres instructions ont quasiment tous FUL=1. Ceci dit, même si une multiplication flottante à simple précision nécessitera 4 cycles pour retourner le résultat, elle occupera l’unité .M pendant 1 seul cycle seulement, par la suite on pourra exécuter d’autres multiplications sur l’unité .M juste dans le cycle qui suit, même si la première multiplication n’a pas encore retourné un résultat, comme schématisé ci-dessous :
On voit alors que ces instructions c66x peuvent fonctionner en "pipeline", on se retrouve finalement avec 1 résultat de multiplication par cycle. Finalement, on fait l’appel à notre routine assembleur à partir du C, en définissant tout d’abord le prototype "souhaité" pour la fonction assembleur, qui par exemple soit défini ainsi : void _myasm(void);
L’appel se fera tout simplement par : _myasm();
On se propose ensuite de faire en assembleur standard c66x, une somme vectorielle de deux vecteurs A et B, de taille N, ayant des éléments flottants à simple précision. Le résultat de la somme est le vecteur noté S. La déclaration et l’allocation de la mémoire pour ces vecteurs serait en C ainsi : #define N 1000 float A[N]; float B[N]; float S[N];
On initialise les vecteurs d’entrée à des valeurs de test ainsi, dans la fonction main : int i; for (i=0;i