Je travaille avec le kit de découverte STM32F303VC et je suis légèrement déconcerté par ses performances. Pour vous familiariser avec le système, j'ai écrit un programme très simple simplement pour tester la vitesse de frappe de ce MCU. Le code peut être décomposé comme suit:
- L'horloge HSI (8 MHz) est activée;
- La PLL est initiée avec le prédécaleur de 16 pour atteindre HSI / 2 * 16 = 64 MHz;
- PLL est désigné comme le SYSCLK;
- SYSCLK est surveillé sur la broche MCO (PA8), et l'une des broches (PE10) est constamment basculée dans la boucle infinie.
Le code source de ce programme est présenté ci-dessous:
#include "stm32f3xx.h"
int main(void)
{
// Initialize the HSI:
RCC->CR |= RCC_CR_HSION;
while(!(RCC->CR&RCC_CR_HSIRDY));
// Initialize the LSI:
// RCC->CSR |= RCC_CSR_LSION;
// while(!(RCC->CSR & RCC_CSR_LSIRDY));
// PLL configuration:
RCC->CFGR &= ~RCC_CFGR_PLLSRC; // HSI / 2 selected as the PLL input clock.
RCC->CFGR |= RCC_CFGR_PLLMUL16; // HSI / 2 * 16 = 64 MHz
RCC->CR |= RCC_CR_PLLON; // Enable PLL
while(!(RCC->CR&RCC_CR_PLLRDY)); // Wait until PLL is ready
// Flash configuration:
FLASH->ACR |= FLASH_ACR_PRFTBE;
FLASH->ACR |= FLASH_ACR_LATENCY_1;
// Main clock output (MCO):
RCC->AHBENR |= RCC_AHBENR_GPIOAEN;
GPIOA->MODER |= GPIO_MODER_MODER8_1;
GPIOA->OTYPER &= ~GPIO_OTYPER_OT_8;
GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR8;
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8;
GPIOA->AFR[0] &= ~GPIO_AFRL_AFRL0;
// Output on the MCO pin:
//RCC->CFGR |= RCC_CFGR_MCO_HSI;
//RCC->CFGR |= RCC_CFGR_MCO_LSI;
//RCC->CFGR |= RCC_CFGR_MCO_PLL;
RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;
// PLL as the system clock
RCC->CFGR &= ~RCC_CFGR_SW; // Clear the SW bits
RCC->CFGR |= RCC_CFGR_SW_PLL; //Select PLL as the system clock
while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL); //Wait until PLL is used
// Bit-bang monitoring:
RCC->AHBENR |= RCC_AHBENR_GPIOEEN;
GPIOE->MODER |= GPIO_MODER_MODER10_0;
GPIOE->OTYPER &= ~GPIO_OTYPER_OT_10;
GPIOE->PUPDR &= ~GPIO_PUPDR_PUPDR10;
GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR10;
while(1)
{
GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;
}
}
Le code a été compilé avec CoIDE V2 avec GNU ARM Embedded Toolchain en utilisant l'optimisation -O1. Les signaux sur les broches PA8 (MCO) et PE10, examinés avec un oscilloscope, ressemblent à ceci:
Le SYSCLK semble être configuré correctement, car le MCO (courbe orange) présente une oscillation de près de 64 MHz (compte tenu de la marge d'erreur de l'horloge interne). La partie bizarre pour moi est le comportement sur PE10 (courbe bleue). Dans la boucle infinie while (1), il faut 4 + 4 + 5 = 13 cycles d'horloge pour effectuer une opération élémentaire en 3 étapes (c'est-à-dire bit-set / bit-reset / return). Cela empire encore sur d'autres niveaux d'optimisation (par exemple -O2, -O3, ar -Os): plusieurs cycles d'horloge supplémentaires sont ajoutés à la partie LOW du signal, c'est-à-dire entre les fronts descendant et montant de PE10 (l'activation du LSI semble en quelque sorte pour remédier à cette situation).
Ce comportement est-il attendu de ce MCU? J'imagine qu'une tâche aussi simple que définir et réinitialiser un peu devrait être 2 à 4 fois plus rapide. Existe-t-il un moyen d'accélérer les choses?
Réponses:
La question ici est vraiment: quel est le code machine que vous générez à partir du programme C, et en quoi diffère-t-il de ce que vous attendez.
Si vous n'aviez pas accès au code d'origine, cela aurait été un exercice de rétro-ingénierie (essentiellement quelque chose commençant par:)
radare2 -A arm image.bin; aaa; VV
, mais vous avez le code, donc cela rend tout plus facile.Tout d'abord, compilez-le avec l'
-g
indicateur ajouté auCFLAGS
(même endroit où vous spécifiez également-O1
). Ensuite, regardez l'assembly généré:Notez bien sûr que le nom du
objdump
binaire ainsi que votre fichier ELF intermédiaire peuvent être différents.Habituellement, vous pouvez également ignorer la partie où GCC appelle l'assembleur et simplement regarder le fichier d'assemblage. Ajoutez simplement
-S
à la ligne de commande GCC - mais cela cassera normalement votre build, donc vous le feriez très probablement en dehors de votre IDE.J'ai fait l' assemblage d'une version légèrement corrigée de votre code :
et obtenu ce qui suit (extrait, code complet sous le lien ci-dessus):
Ce qui est une boucle (notez le saut inconditionnel vers .L5 à la fin et l'étiquette .L5 au début).
Ce que nous voyons ici, c'est que nous
ldr
(registre de chargement) le registrer2
avec la valeur à l'emplacement de mémoire stockée dansr3
+ 24 octets. Être trop paresseux pour chercher ça: très probablement l'emplacement deBSRR
.OR
ler2
registre avec la constante1024 == (1<<10)
, ce qui correspondrait à la définition du 10e bit dans ce registre, et écrire le résultat àr2
lui-même.str
(stockez) le résultat dans l'emplacement de mémoire que nous avons lu à la première étapeBRR
.b
(branche) retour à la première étape.Nous avons donc 7 instructions, pas trois, pour commencer. Cela ne
b
se produit qu'une seule fois, et c'est donc très probablement ce qui prend un nombre impair de cycles (nous en avons 13 au total, donc quelque part un nombre de cycles impair doit provenir). Étant donné que tous les nombres impairs inférieurs à 13 sont 1, 3, 5, 7, 9, 11, et nous pouvons exclure tout nombre supérieur à 13-6 (en supposant que le CPU ne peut pas exécuter une instruction en moins d'un cycle), nous savons que leb
prend 1, 3, 5 ou 7 cycles CPU.Étant qui nous sommes, j'ai regardé la documentation d' instructions d'ARM et combien de cycles ils prennent pour le M3:
ldr
prend 2 cycles (dans la plupart des cas)orr
prend 1 cyclestr
prend 2 cyclesb
prend 2 à 4 cycles. Nous savons que ce doit être un nombre impair, donc il doit en prendre 3, ici.Tout cela correspond à votre observation:
Comme le montre le calcul ci-dessus, il n'y aura guère de moyen de rendre votre boucle plus rapide - les broches de sortie des processeurs ARM sont généralement mappées en mémoire , pas les registres de base du processeur, vous devez donc passer par la routine habituelle de chargement - modification - stockage si vous voulez tout faire avec ça.
Ce que vous pourriez bien sûr faire, ce n'est pas lire ( doit
|=
implicitement lire) la valeur de la broche à chaque itération de boucle, mais simplement y écrire la valeur d'une variable locale, que vous basculez simplement à chaque itération de boucle.Notez que je pense que vous connaissez peut-être les micros 8 bits et que vous essayez de lire uniquement les valeurs 8 bits, de les stocker dans des variables locales 8 bits et de les écrire en blocs 8 bits. Non. ARM est une architecture 32 bits, et l'extraction de 8 bits d'un mot 32 bits peut prendre des instructions supplémentaires. Si vous le pouvez, il vous suffit de lire le mot entier de 32 bits, de modifier ce dont vous avez besoin et de le réécrire en entier. Bien sûr, cela dépend de ce que vous écrivez, c'est-à-dire de la disposition et des fonctionnalités de votre GPIO mappé en mémoire. Consultez la fiche technique / le guide de l'utilisateur du STM32F3 pour plus d'informations sur ce qui est stocké dans le 32 bits contenant le bit que vous souhaitez basculer.
Maintenant, j'ai essayé de reproduire votre problème avec la période de « faible » se allongent, mais je ne pouvais tout simplement pas - les regards de boucle exactement la même chose avec
-O3
comme avec-O1
ma version du compilateur. Vous devrez le faire vous-même! Peut-être que vous utilisez une ancienne version de GCC avec un support ARM sous-optimal.la source
=
au lieu de|=
), comme vous le dites, ne serait-il pas exactement l'accélération que l'OP recherche? La raison pour laquelle les ARM ont les registres BRR et BSRR séparément est de ne pas nécessiter de lecture-modification-écriture. Dans ce cas, les constantes pourraient être stockées dans des registres à l'extérieur de la boucle, de sorte que la boucle intérieure ne serait que de 2 str et d'une branche, donc 2 + 2 +3 = 7 cycles pour tout le cycle?-O3
erreur semble avoir disparu après le nettoyage et la reconstruction de la solution. Néanmoins, mon code d'assemblage semble contenir une instruction UTXH supplémentaire:.L5:
ldrh r3, [r2, #24]
uxth r3, r3
orr r3, r3, #1024
strh r3, [r2, #24] @ movhi
ldr r3, [r2, #40]
orr r3, r3, #1024
str r3, [r2, #40]
b .L5
uxth
est là carGPIO->BSRRL
est (incorrectement) défini comme un registre 16 bits dans vos en-têtes. Utilisez une version récente des en-têtes, provenant des bibliothèques STM32CubeF3 , où il n'y a pas de BSRRL et BSRRH, mais un seulBSRR
registre 32 bits . @Marcus a apparemment les en-têtes corrects, donc son code fait des accès complets en 32 bits au lieu de charger un demi-mot et de l'étendre.LDRB
etSTRB
qui effectue des lectures / écritures d'octets dans une seule instruction, non?Les registres
BSRR
etBRR
permettent de définir et de réinitialiser les bits de port individuels:Comme vous pouvez le voir, la lecture de ces registres donne toujours 0, donc quel est votre code
n'est efficace
GPIOE->BRR = 0 | GPIO_BRR_BR_10
, mais l'optimiseur ne sait pas que, il génère une séquence deLDR
,ORR
, desSTR
instructions au lieu d'un seul magasin.Vous pouvez éviter l'opération coûteuse de lecture-modification-écriture en écrivant simplement
Vous pouvez obtenir une amélioration supplémentaire en alignant la boucle sur une adresse divisible par 8. Essayez de mettre une ou des
asm("nop");
instructions de mode avant lawhile(1)
boucle.la source
Pour ajouter à ce qui a été dit ici: Certainement avec le Cortex-M, mais à peu près n'importe quel processeur (avec un pipeline, un cache, une prédiction de branche ou d'autres fonctionnalités), il est trivial de prendre même la boucle la plus simple:
Exécutez-le autant de millions de fois que vous le souhaitez, mais soyez en mesure de faire varier considérablement les performances de cette boucle, juste ces deux instructions, ajoutez quelques nops au milieu si vous le souhaitez; ça n'a pas d'importance.
Changer l'alignement de la boucle peut faire varier considérablement les performances, en particulier avec une petite boucle comme celle-là, si cela prend deux lignes de récupération au lieu d'une, vous mangez ce coût supplémentaire, sur un microcontrôleur comme celui-ci où le flash est plus lent que le CPU de 2 ou 3, puis en augmentant l'horloge, le rapport devient encore pire 3 ou 4 ou 5 que l'ajout de l'extraction supplémentaire.
Vous n'avez probablement pas de cache, mais si vous l'aviez, cela aide dans certains cas, mais cela fait mal dans d'autres et / ou ne fait aucune différence. La prédiction de branche que vous pouvez ou non avoir ici (probablement pas) ne peut être vue que dans la mesure où elle est conçue dans le tuyau, donc même si vous avez changé la boucle pour créer une branche et que vous aviez une branche inconditionnelle à la fin (plus facile pour un prédicteur de branche de utiliser) tout ce que cela fait est de vous faire économiser autant d'horloges (taille du tuyau d'où il irait normalement jusqu'à la profondeur que le prédicteur peut voir) lors de la prochaine extraction et / ou il ne fait pas de prélecture juste au cas où.
En modifiant l'alignement par rapport aux lignes d'extraction et de cache, vous pouvez déterminer si le prédicteur de branche vous aide ou non, et cela peut être vu dans les performances globales, même si vous ne testez que deux instructions ou les deux avec quelques nops .
C'est un peu trivial de le faire, et une fois que vous comprenez que, puis en prenant du code compilé, ou même un assemblage écrit à la main, vous pouvez voir que ses performances peuvent varier considérablement en raison de ces facteurs, en ajoutant ou en enregistrant quelques-uns à quelques centaines de pour cent, une ligne de code C, un nop mal placé.
Après avoir appris à utiliser le registre BSRR, essayez d'exécuter votre code à partir de la RAM (copier et sauter) au lieu du flash, ce qui devrait vous donner un gain de performances instantané de 2 à 3 fois dans l'exécution sans rien faire d'autre.
la source
C'est un comportement de votre code.
Vous devez écrire dans les registres BRR / BSRR, pas lire-modifier-écrire comme vous le faites maintenant.
Vous subissez également des frais généraux de boucle. Pour des performances optimales, répliquez les opérations BRR / BSRR à plusieurs reprises → copiez-collez-les plusieurs fois dans la boucle afin de passer par de nombreux cycles de définition / réinitialisation avant une surcharge de boucle.
edit: quelques tests rapides sous IAR.
un basculement dans l'écriture dans BRR / BSRR nécessite 6 instructions sous optimisation modérée et 3 instructions sous le niveau d'optimisation le plus élevé; un feuilletage RMW'ng prend 10 instructions / 6 instructions.
boucle aérienne supplémentaire.
la source
|=
à=
une phase de définition / réinitialisation d' un seul bit, 9 cycles d'horloge ( liaison ) sont nécessaires . Le code de montage comprend 3 instructions:.L5
strh r1, [r3, #24] @ movhi
str r2, [r3, #40]
b .L5
gcc -funroll-loops
) peuvent très bien faire, et que lorsqu'il est abusé (comme ici) a l'effet inverse de ce que vous voulez.somePortLatch
contrôle un port dont les 4 bits inférieurs sont définis pour la sortie, il peut être possible de déroulerwhile(1) { SomePortLatch ^= (ctr++); }
dans un code qui génère 15 valeurs, puis de revenir en arrière pour démarrer au moment où il aurait sinon généré la même valeur deux fois de suite.