Prestazioni veloci da un MCU STM32


11

Sto lavorando con il kit di rilevamento STM32F303VC e sono leggermente perplesso dalle sue prestazioni. Per conoscere il sistema, ho scritto un programma molto semplice semplicemente per testare la velocità di bit-bang di questo MCU. Il codice può essere suddiviso come segue:

  1. Il clock HSI (8 MHz) è attivato;
  2. PLL viene avviato con il prescaler di 16 per ottenere HSI / 2 * 16 = 64 MHz;
  3. PLL è designato come SYSCLK;
  4. SYSCLK viene monitorato sul pin MCO (PA8) e uno dei pin (PE10) viene costantemente attivato / disattivato nel loop infinito.

Il codice sorgente per questo programma è presentato di seguito:

#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;

      }
}

Il codice è stato compilato con CoIDE V2 con GNU ARM Embedded Toolchain usando l'ottimizzazione -O1. I segnali sui pin PA8 (MCO) e PE10, esaminati con un oscilloscopio, si presentano così: inserisci qui la descrizione dell'immagine

Il SYSCLK sembra essere configurato correttamente, poiché l'MCO (curva arancione) presenta un'oscillazione di quasi 64 MHz (considerando il margine di errore del clock interno). La parte strana per me è il comportamento su PE10 (curva blu). Nell'infinito ciclo while (1) sono necessari 4 + 4 + 5 = 13 cicli di clock per eseguire un'operazione elementare in 3 fasi (ovvero bit-set / bit-reset / return). Peggiora ancora su altri livelli di ottimizzazione (es. -O2, -O3, ar -Os): diversi cicli di clock aggiuntivi vengono aggiunti alla parte LOW del segnale, cioè tra i fronti di caduta e di salita di PE10 (abilitando in qualche modo l'LSI per porre rimedio a questa situazione).

Questo comportamento è previsto da questo MCU? Immagino che un'attività semplice come impostare e ripristinare un po 'dovrebbe essere 2-4 volte più veloce. C'è un modo per accelerare le cose?


Hai provato con qualche altro MCU per confrontare?
Marko Buršič,

3
Cosa stai cercando di ottenere? Se si desidera un'uscita a oscillazione rapida, è necessario utilizzare i timer. Se si desidera interfacciarsi con protocolli seriali veloci, è necessario utilizzare la periferica hardware corrispondente.
Jonas Schäfer,

2
Ottimo inizio con il kit !!
Scott Seidman,

Non devi | = registri BSRR o BRR poiché sono solo di scrittura.
P__J__

Risposte:


25

La domanda qui è davvero: qual è il codice macchina che stai generando dal programma C e come differisce da quello che ti aspetteresti.

Se non avessi accesso al codice originale, questo sarebbe stato un esercizio di ingegneria inversa (fondamentalmente qualcosa che inizia con:) radare2 -A arm image.bin; aaa; VV, ma hai il codice in modo da rendere tutto più semplice.

Innanzitutto, compilarlo con il -gflag aggiunto allo CFLAGS(stesso posto in cui si specifica anche -O1). Quindi, guarda l'assembly generato:

arm-none-eabi-objdump -S yourprog.elf

Si noti che, naturalmente, sia il nome del file objdumpbinario che il file ELF intermedio potrebbero essere diversi.

Di solito, puoi anche saltare la parte in cui GCC invoca l'assemblatore e guardare il file di assieme. Basta aggiungere -Salla riga di comando di GCC, ma questo normalmente interromperà la tua build, quindi molto probabilmente lo faresti al di fuori del tuo IDE.

Ho fatto il montaggio di una versione leggermente modificata del tuo codice :

arm-none-eabi-gcc 
    -O1 ## your optimization level
    -S  ## stop after generating assembly, i.e. don't run `as`
    -I/path/to/CMSIS/ST/STM32F3xx/ -I/path/to/CMSIS/include
     test.c

e ottenuto il seguente (estratto, codice completo sotto il link sopra):

.L5:
    ldr r2, [r3, #24]
    orr r2, r2, #1024
    str r2, [r3, #24]
    ldr r2, [r3, #40]
    orr r2, r2, #1024
    str r2, [r3, #40]
    b   .L5

Che è un loop (notare il salto incondizionato a .L5 alla fine e l'etichetta .L5 all'inizio).

Quello che vediamo qui è che noi

  • prima ldr(registro di carico) il registro r2con il valore nella posizione di memoria memorizzato in r3+ 24 byte. Essere troppo pigri per cercarlo: molto probabilmente la posizione di BSRR.
  • Quindi ORil r2registro con la costante 1024 == (1<<10), che corrisponde all'impostazione del decimo bit in quel registro, e scrive il risultato su r2se stesso.
  • Quindi str(memorizza) il risultato nella posizione di memoria da cui abbiamo letto nel primo passaggio
  • e poi ripeti lo stesso per una diversa posizione di memoria, per pigrizia: molto probabilmente BRRl'indirizzo.
  • Infine b(ramo) torna al primo passo.

Quindi abbiamo 7 istruzioni, non tre, per cominciare. Solo il baccade una volta, e quindi è molto probabile che quello che sta prendendo un numero dispari di cicli (abbiamo 13 in totale, in modo da qualche parte un conteggio di ciclo dispari deve venire da). Poiché tutti i numeri dispari inferiori a 13 sono 1, 3, 5, 7, 9, 11 e possiamo escludere qualsiasi numero maggiore di 13-6 (supponendo che la CPU non possa eseguire un'istruzione in meno di un ciclo), sappiamo che brichiede 1, 3, 5 o 7 cicli CPU.

Essendo quello che siamo, ho guardato la documentazione di istruzioni ARM e quanti cicli prendono per M3:

  • ldr richiede 2 cicli (nella maggior parte dei casi)
  • orr richiede 1 ciclo
  • str dura 2 cicli
  • bdura da 2 a 4 cicli. Sappiamo che deve essere un numero dispari, quindi devono essere 3, qui.

Tutto ciò si allinea alla tua osservazione:

13=2(cldr+corr+cstr)+cb=2(2+1+2)+3=25+3

Come mostra il calcolo sopra, difficilmente ci sarà un modo per rendere il tuo ciclo più veloce - i pin di uscita sui processori ARM sono solitamente mappati in memoria , non registri core della CPU, quindi devi passare attraverso il solito carico - modificare - memorizzare la routine se vuoi fare qualsiasi cosa con quelli.

Ciò che si potrebbe ovviamente fare non è leggere ( |=implicitamente deve leggere) il valore del pin in ogni iterazione di loop, ma semplicemente scrivere il valore di una variabile locale su di essa, che si attiva e disattiva ogni iterazione di loop.

Si noti che sento che potresti avere familiarità con i micro a 8 bit e che tenteresti di leggere solo valori a 8 bit, memorizzarli in variabili locali a 8 bit e scriverli in blocchi di 8 bit. Non farlo. ARM è un'architettura a 32 bit e l'estrazione di 8 bit di una parola a 32 bit potrebbe richiedere istruzioni aggiuntive. Se puoi, leggi l'intera parola a 32 bit, modifica ciò di cui hai bisogno e scrivila come intera. Se ciò è possibile, ovviamente, dipende da cosa stai scrivendo, ovvero dal layout e dalla funzionalità del tuo GPIO mappato in memoria. Consultare la scheda tecnica / la guida dell'utente STM32F3 per informazioni su cosa è memorizzato a 32 bit contenente il bit che si desidera attivare.


Ora, ho provato a riprodurre il tuo problema con il periodo "basso" che si allungava, ma semplicemente non ci riuscivo: il ciclo sembra esattamente lo stesso di -O3quello -O1con la mia versione del compilatore. Dovrai farlo tu stesso! Forse stai usando una versione antica di GCC con supporto ARM non ottimale.


4
La memorizzazione ( =invece di |=) non sarebbe , come dici tu, esattamente la velocità che l'OP sta cercando? Il motivo per cui gli ARM hanno i registri BRR e BSRR separatamente non è necessario leggere-modificare-scrivere. In questo caso, le costanti potrebbero essere memorizzate in registri esterni al loop, quindi il loop interno sarebbe solo 2 str e un ramo, quindi 2 + 2 +3 = 7 cicli per l'intero round?
Timo

Grazie. Ciò ha chiarito un bel po 'le cose. Era un po 'frettoloso pensare di insistere sul fatto che sarebbero stati necessari solo 3 cicli di clock: da 6 a 7 cicli erano qualcosa che speravo davvero. L' -O3errore sembra essere scomparso dopo aver pulito e ricostruito la soluzione. Tuttavia, il mio codice assembly sembra contenere al suo interno un'istruzione UTXH aggiuntiva: .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
KR

1
uxthesiste perché GPIO->BSRRLè (erroneamente) definito come un registro a 16 bit nelle intestazioni. Utilizzare una versione recente delle intestazioni, dalle librerie STM32CubeF3 , dove non sono presenti BSRRL e BSRRH, ma un singolo BSRRregistro a 32 bit . Apparentemente @Marcus ha le intestazioni corrette, quindi il suo codice esegue accessi a 32 bit invece di caricare una mezza parola ed estenderla.
berendi - protestando contro il

Perché il caricamento di un singolo byte richiede istruzioni aggiuntive? L'architettura ARM ha LDRBe STRBche esegue letture / scritture di byte in una singola istruzione, no?
psmears,

1
Il core M3 può supportare il bit-banding (non sono sicuro che questa particolare implementazione lo faccia), in cui una regione di 1 MB di spazio di memoria periferica è aliasata a una regione di 32 MB. Ogni bit ha un indirizzo di parola discreto (viene utilizzato solo il bit 0). Presumibilmente ancora più lento di un semplice carico / negozio.
Sean Houlihane,

8

I registri BSRRe BRRservono per impostare e ripristinare singoli bit di porta:

Registro set / reset bit porta GPIO (GPIOx_BSRR)

...

(x = A..H) Bit 15: 0

BSy: porta x imposta bit y (y = 0..15)

Questi bit sono di sola scrittura. Una lettura di questi bit restituisce il valore 0x0000.

0: nessuna azione sul bit ODRx corrispondente

1: imposta il bit ODRx corrispondente

Come puoi vedere, leggere questi registri dà sempre 0, quindi qual è il tuo codice

GPIOE->BSRRL |= GPIO_BSRR_BS_10;
GPIOE->BRR |= GPIO_BRR_BR_10;

fa in modo efficace è GPIOE->BRR = 0 | GPIO_BRR_BR_10, ma l'ottimizzatore non sa che, in modo che genera una sequenza di LDR, ORR, STRistruzioni invece di un unico negozio.

Puoi evitare la costosa operazione di lettura-modifica-scrittura semplicemente scrivendo

GPIOE->BSRRL = GPIO_BSRR_BS_10;
GPIOE->BRR = GPIO_BRR_BR_10;

Potresti ottenere ulteriori miglioramenti allineando il loop a un indirizzo uniformemente divisibile per 8. Prova a mettere una o le asm("nop");istruzioni di modalità prima del while(1)loop.


1

Per aggiungere a ciò che è stato detto qui: Certamente con Cortex-M, ma praticamente qualsiasi processore (con una pipeline, cache, previsione del ramo o altre funzionalità), è banale prendere anche il ciclo più semplice:

top:
   subs r0,#1
   bne top

Eseguilo tutte le milioni di volte che vuoi, ma essere in grado di far variare notevolmente le prestazioni di quel ciclo, solo quelle due istruzioni, se vuoi aggiungere qualche nops nel mezzo; non importa.

La modifica dell'allineamento del loop può variare drasticamente le prestazioni, specialmente con un loop piccolo come quello se prende due linee di recupero invece di una, si consuma quel costo extra, su un microcontrollore come questo dove il flash è più lento della CPU di 2 o 3 e quindi aumentando l'orologio il rapporto peggiora ancora di 3 o 4 o 5 rispetto all'aggiunta del recupero extra.

Probabilmente non hai una cache, ma se lo hai fatto aiuta in alcuni casi, ma fa male in altri e / o non fa differenza. La previsione della diramazione che potresti avere o che non puoi avere qui (probabilmente no) può vedere solo nella misura in cui è stata progettata nella pipe, quindi anche se hai modificato il loop in diramazione e hai avuto un branch incondizionato alla fine (più facile per un predittore di branch a usa) tutto ciò che fa è salvarti quel numero di orologi (dimensioni della pipe da cui normalmente verrebbe a sapere quanto in profondità può vedere il predittore) al prossimo recupero e / o non esegue un prefetch per ogni evenienza.

Modificando l'allineamento rispetto alle linee di recupero e cache puoi influenzare se il predittore di ramo ti sta aiutando o meno e ciò può essere visto nelle prestazioni complessive, anche se stai solo testando due istruzioni o quelle due con alcune no .

È in qualche modo banale fare questo, e una volta capito che, prendendo quindi il codice compilato o anche l'assemblaggio scritto a mano, puoi vedere che le sue prestazioni possono variare ampiamente a causa di questi fattori, aggiungendo o salvando un paio al duecento per cento, una riga di codice C, una no posizionata male.

Dopo aver imparato ad usare il registro BSRR, prova a eseguire il tuo codice dalla RAM (copia e salta) invece di Flash che dovrebbe darti un potenziamento istantaneo delle prestazioni da 2 a 3 volte nell'esecuzione senza fare altro.


0

Questo comportamento è previsto da questo MCU?

È un comportamento del tuo codice.

  1. Dovresti scrivere nei registri BRR / BSRR, non leggere-modificare-scrivere come fai ora.

  2. Si incorre anche in un sovraccarico di loop. Per ottenere le massime prestazioni, replicare ripetutamente le operazioni BRR / BSRR → copiarle e incollarle nel loop più volte in modo da passare attraverso molti cicli di set / reset prima di un overhead del loop.

modifica: alcuni test rapidi sotto IAR.

uno sfogliare la scrittura su BRR / BSRR richiede 6 istruzioni con ottimizzazione moderata e 3 istruzioni con il massimo livello di ottimizzazione; un giro attraverso RMW'ng richiede 10 istruzioni / 6 istruzioni.

loop overhead extra.


Passando |=a =una fase di set / reset a singolo bit si consumano 9 cicli di clock ( collegamento ). Il codice assembly è lungo 3 istruzioni:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
Non srotolare manualmente i loop. Non è praticamente mai una buona idea. In questo caso particolare, è particolarmente disastroso: rende la forma d'onda non periodica. Inoltre, avere lo stesso codice molte volte in flash non è necessariamente più veloce. Questo potrebbe non applicarsi qui (potrebbe!), Ma lo srotolamento in loop è qualcosa che molte persone pensano possa aiutare, che i compilatori ( gcc -funroll-loops) possono fare molto bene e che se abusati (come qui) ha l'effetto inverso di ciò che vuoi.
Marcus Müller,

Un loop infinito non può mai essere srotolato efficacemente per mantenere un comportamento di temporizzazione coerente.
Marcus Müller,

1
@ MarcusMüller: i loop infiniti possono talvolta essere srotolati utilmente mantenendo un tempismo coerente se ci sono punti in alcune ripetizioni del loop in cui un'istruzione non avrebbe alcun effetto visibile. Ad esempio, se somePortLatchcontrolla una porta i cui 4 bit inferiori sono impostati per l'output, potrebbe essere possibile srotolare il while(1) { SomePortLatch ^= (ctr++); }codice che genera 15 valori e quindi tornare indietro per iniziare nel momento in cui altrimenti emetterebbe lo stesso valore due volte di seguito.
supercat

Supercat, vero. Inoltre, effetti come il timing dell'interfaccia di memoria ecc. Potrebbero rendere sensato lo srotolamento "parziale". La mia affermazione era troppo generica, ma sento che il consiglio di Danny è ancora più generalizzante, e anche pericolosamente
Marcus Müller
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.