Obtendo desempenho rápido de um MCU STM32


11

Estou trabalhando com o kit de descoberta STM32F303VC e estou um pouco intrigado com seu desempenho. Para me familiarizar com o sistema, escrevi um programa muito simples, simplesmente para testar a velocidade desse MCU. O código pode ser dividido da seguinte maneira:

  1. O relógio HSI (8 MHz) está ativado;
  2. O PLL é iniciado com o com o pré-calibrador de 16 para atingir HSI / 2 * 16 = 64 MHz;
  3. PLL é designado como SYSCLK;
  4. O SYSCLK é monitorado no pino MCO (PA8), e um dos pinos (PE10) é constantemente alternado no loop infinito.

O código fonte deste programa é apresentado abaixo:

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

      }
}

O código foi compilado com o CoIDE V2 com o GNU ARM Embedded Toolchain usando a otimização -O1. Os sinais nos pinos PA8 (MCO) e PE10, examinados com um osciloscópio, têm a seguinte aparência: insira a descrição da imagem aqui

O SYSCLK parece estar configurado corretamente, pois o MCO (curva laranja) exibe uma oscilação de quase 64 MHz (considerando a margem de erro do relógio interno). A parte estranha para mim é o comportamento no PE10 (curva azul). No loop while (1) infinito, são necessários 4 + 4 + 5 = 13 ciclos de relógio para executar uma operação elementar de 3 etapas (isto é, set-bit / bit-reset / return). Fica ainda pior em outros níveis de otimização (por exemplo, -O2, -O3, ar -Os): vários ciclos de clock adicionais são adicionados à parte LOW do sinal, ou seja, entre as bordas crescente e decrescente do PE10 (permitindo que o LSI de alguma forma pareça para remediar esta situação).

Esse comportamento é esperado deste MCU? Eu imagino que uma tarefa tão simples quanto definir e redefinir um pouco deve ser 2-4 vezes mais rápida. Existe uma maneira de acelerar as coisas?


Você já tentou com algum outro MCU comparar?
Marko Buršič 28/03

3
O que você está tentando alcançar? Se você deseja uma saída oscilante rápida, deve usar temporizadores. Se você deseja interagir com protocolos seriais rápidos, deve usar o periférico de hardware correspondente.
Jonas Schäfer

2
Ótimo começo com o kit !!
Scott Seidman

Você não deve registrar registros BSRR ou BRR, pois eles são somente gravação.
P__J__

Respostas:


25

A questão aqui é realmente: qual é o código de máquina que você está gerando do programa C e como ele difere do que você esperaria.

Se você não tivesse acesso ao código original, isso seria um exercício de engenharia reversa (basicamente algo que começa com :) radare2 -A arm image.bin; aaa; VV, mas você tem o código, o que facilita tudo isso.

Primeiro, compile-o com o -gsinalizador adicionado ao CFLAGS(mesmo local em que você também especifica -O1). Em seguida, observe o assembly gerado:

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

Observe que é claro que tanto o nome do objdumparquivo binário quanto o arquivo ELF intermediário podem ser diferentes.

Geralmente, você também pode pular a parte em que o GCC chama o assembler e apenas olhar o arquivo do assembly. Basta adicionar -Sà linha de comando do GCC - mas isso normalmente interromperá a sua compilação; portanto, você provavelmente faria isso fora do seu IDE.

Fiz a montagem de uma versão ligeiramente corrigida do seu código :

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 obteve o seguinte (trecho, código completo no link acima):

.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

Que é um loop (observe o salto incondicional para .L5 no final e o rótulo .L5 no início).

O que vemos aqui é que nós

  • primeiro ldr(registro de carga) o registro r2com o valor no local da memória armazenado em r3+ 24 bytes. Sendo muito preguiçoso para procurar isso: muito provavelmente a localização de BSRR.
  • Em seguida, ORo r2indicador com a constante 1024 == (1<<10), o que corresponderia a definir o dia 10 bit no referido registo, e escrever o resultado para r2si.
  • Em seguida, strarmazene o resultado no local da memória que lemos na primeira etapa
  • e repita o mesmo para um local de memória diferente, por preguiça: provavelmente BRRo endereço.
  • Finalmente b(ramificação), volte para o primeiro passo.

Portanto, temos 7 instruções, não três, para começar. Apenas isso bacontece uma vez e, portanto, é muito provável que esteja ocorrendo um número ímpar de ciclos (temos 13 no total, portanto, em algum lugar, uma contagem ímpar de ciclos deve vir). Como todos os números ímpares abaixo de 13 são 1, 3, 5, 7, 9, 11, e podemos excluir números maiores que 13-6 (supondo que a CPU não possa executar uma instrução em menos de um ciclo), sabemos que que bleva 1, 3, 5 ou 7 ciclos da CPU.

Sendo quem somos, olhei para a documentação de instruções do ARM e quanto ciclo leva para o M3:

  • ldr leva 2 ciclos (na maioria dos casos)
  • orr leva 1 ciclo
  • str leva 2 ciclos
  • bleva 2 a 4 ciclos. Sabemos que deve ser um número ímpar, então deve levar 3 aqui.

Que tudo se alinha com a sua observação:

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

Como o cálculo acima mostra, dificilmente haverá uma maneira de tornar seu loop mais rápido - os pinos de saída nos processadores ARM geralmente são mapeados na memória , não nos registros do núcleo da CPU, então você deve seguir a rotina usual de carregar - modificar - armazenar se você quer fazer algo com isso.

Obviamente, o que você pode fazer não é ler ( |=implicitamente precisa ler) o valor do pino a cada iteração de loop, mas apenas escrever o valor de uma variável local nela, que você apenas alterna em cada iteração de loop.

Observe que eu sinto que você pode estar familiarizado com micros de 8 bits e que tentaria ler apenas valores de 8 bits, armazená-los em variáveis ​​locais de 8 bits e gravá-los em pedaços de 8 bits. Não. O ARM é uma arquitetura de 32 bits e a extração de 8 bits de uma palavra de 32 bits pode exigir instruções adicionais. Se puder, basta ler a palavra inteira de 32 bits, modificar o que você precisa e escrevê-la novamente. Se isso é possível, é claro, depende do que você está escrevendo, ou seja, o layout e a funcionalidade do seu GPIO mapeado na memória. Consulte a folha de dados / guia do usuário do STM32F3 para obter informações sobre o que está armazenado nos 32 bits que contêm o bit que você deseja alternar.


Agora, eu tentei reproduzir o problema com o período de "baixa" a ficar mais tempo, mas eu simplesmente não podia - os olhares de loop exatamente o mesmo com -O3como com -O1com a minha versão do compilador. Você terá que fazer isso sozinho! Talvez você esteja usando uma versão antiga do GCC com suporte a ARM abaixo do ideal.


4
Armazenar (em =vez de |=) não seria exatamente o que o OP está procurando? A razão pela qual os ARMs possuem os registros BRR e BSRR separadamente é não exigir leitura-modificação-gravação. Nesse caso, as constantes poderiam ser armazenadas em registradores fora do loop, então o loop interno seria apenas 2 strs e um branch, então 2 + 2 +3 = 7 ciclos para toda a rodada?
Timo

Obrigado. Isso realmente esclareceu as coisas um pouco. Foi um pouco de pressa pensar insistir em que apenas 3 ciclos de relógio seriam necessários - 6 a 7 ciclos eram algo que eu estava realmente esperando. O -O3erro parece ter desaparecido após a limpeza e reconstrução da solução. No entanto, meu código de montagem parece ter uma instrução UTXH adicional:.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
uxthexiste porque GPIO->BSRRLé (incorretamente) definido como um registro de 16 bits em seus cabeçalhos. Use uma versão recente dos cabeçalhos, das bibliotecas STM32CubeF3 , em que não há BSRRL e BSRRH, mas um único BSRRregistro de 32 bits . @ Marcus aparentemente tem os cabeçalhos corretos, então seu código acessa 32 bits em vez de carregar uma meia palavra e estendê-la.
berendi - protestando em 28/03

Por que carregar um único byte precisaria de instruções extras? A arquitetura do ARM possui LDRBe STRBque executa leituras / gravações de bytes em uma única instrução, não?
Psmears 28/03

1
O núcleo M3 pode suportar bandas de bits (não tenho certeza se essa implementação específica o faz), onde uma região de 1 MB de espaço de memória periférica é aliasizada a uma região de 32 MB. Cada bit possui um endereço de palavra discreto (o bit 0 é usado apenas). Presumivelmente ainda mais lento do que apenas uma carga / armazenamento.
Sean Houlihane 29/03

8

Os registros BSRRe BRRsão para definir e redefinir bits de porta individuais:

Conjunto de bits da porta GPIO / registro de redefinição (GPIOx_BSRR)

...

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

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

Esses bits são somente para gravação. Uma leitura para esses bits retorna o valor 0x0000.

0: Nenhuma ação no bit ODRx correspondente

1: define o bit ODRx correspondente

Como você pode ver, a leitura desses registros sempre dá 0, portanto, qual é o seu código

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

faz de forma eficaz é GPIOE->BRR = 0 | GPIO_BRR_BR_10, mas o otimizador não sabe que, por isso gera uma sequência de LDR, ORR, STRinstruções, em vez de uma única loja.

Você pode evitar a dispendiosa operação de leitura, modificação e gravação, simplesmente escrevendo

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

Você pode obter mais melhorias alinhando o loop a um endereço igualmente divisível por 8. Tente colocar uma ou as asm("nop");instruções de modo antes do while(1)loop.


1

Para adicionar ao que foi dito aqui: certamente com o Cortex-M, mas praticamente qualquer processador (com um pipeline, cache, previsão de ramificação ou outros recursos), é trivial fazer o loop mais simples:

top:
   subs r0,#1
   bne top

Execute-o quantas vezes quiser, mas seja possível que o desempenho desse loop varie amplamente, apenas essas duas instruções; adicione alguns nops no meio, se desejar; Não importa.

Alterar o alinhamento do loop pode variar drasticamente o desempenho, especialmente com um loop pequeno como esse, se for preciso duas linhas de busca em vez de uma, você gasta esse custo extra em um microcontrolador como este, onde o flash é mais lento que a CPU por 2 ou 3 e, aumentando o relógio, a proporção fica ainda pior 3 ou 4 ou 5 do que adicionar busca extra.

Você provavelmente não tem um cache, mas se você o tiver ajudado em alguns casos, mas dói em outros e / ou não faz diferença. A previsão de ramificação que você pode ou não ter aqui (provavelmente não) pode apenas ver até onde foi projetada no canal, portanto, mesmo se você alterou o loop para ramificar e tivesse uma ramificação incondicional no final (mais fácil para um preditor de ramificação use) tudo o que faz é poupar o número de relógios (tamanho do tubo de onde ele normalmente alcançaria até a profundidade que o preditor pode ver) na próxima busca e / ou não fará uma pré-busca apenas por precaução.

Alterando o alinhamento em relação às linhas de busca e cache, você pode afetar se o preditor de ramificação está ajudando ou não, e isso pode ser visto no desempenho geral, mesmo se você estiver testando apenas duas instruções ou aquelas com alguns nops .

É um tanto trivial fazer isso e, depois de entender que, depois de pegar o código compilado ou mesmo o conjunto escrito à mão, você pode ver que seu desempenho pode variar amplamente devido a esses fatores, adicionando ou economizando algumas centenas de por cento, uma linha de código C, uma mal posicionada.

Depois de aprender a usar o registro BSRR, tente executar seu código da RAM (copiar e pular) em vez do flash, o que deve proporcionar um aumento instantâneo de 2 a 3 vezes o desempenho na execução sem fazer mais nada.


0

Esse comportamento é esperado deste MCU?

É um comportamento do seu código.

  1. Você deve gravar nos registros BRR / BSRR, não ler-modificar-gravar como faz agora.

  2. Você também incorre em sobrecarga de loop. Para obter o desempenho máximo, replique as operações BRR / BSRR repetidamente → copie e cole no loop várias vezes, para que você passe por muitos ciclos de ajuste / redefinição antes de um loop sobrecarregar.

editar: alguns testes rápidos no IAR.

um avanço na escrita para BRR / BSRR leva 6 instruções sob otimização moderada e 3 instruções sob alto nível de otimização; uma mudança no RMW'ng leva 10 instruções / 6 instruções.

sobrecarga de loop extra.


Ao mudar |=para =uma única fase de configuração / reposição de bit, consome 9 ciclos de relógio ( link ). O código de montagem tem três instruções:.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
Não desenrole manualmente os loops. Isso praticamente nunca é uma boa ideia. Nesse caso em particular, é especialmente desastroso: torna a forma de onda não periódica. Além disso, ter o mesmo código muitas vezes em flash não é necessariamente mais rápido. Isso pode não se aplicar aqui (pode ser!), Mas o desenrolar de loop é algo que muitas pessoas acham que ajuda, que os compiladores ( gcc -funroll-loops) podem fazer muito bem e que quando abusados ​​(como aqui) têm o efeito inverso do que você deseja.
Marcus Müller

Um loop infinito nunca pode ser desenrolado efetivamente para manter um comportamento consistente de tempo.
Marcus Müller

1
@ MarcusMüller: Loops infinitos às vezes podem ser desenrolados de maneira útil, mantendo um tempo consistente, se houver algum ponto em algumas repetições do loop em que uma instrução não teria efeito visível. Por exemplo, se somePortLatchcontrola uma porta cujos 4 bits inferiores estão configurados para saída, pode ser possível desenrolar while(1) { SomePortLatch ^= (ctr++); }no código que gera 15 valores e, em seguida, retornar ao início no momento em que, caso contrário, seria o mesmo valor duas vezes seguidas.
Supercat 28/03

Supercat, é verdade. Além disso, efeitos como o tempo da interface da memória, etc. podem fazer com que seja "parcialmente" desenrolado. Minha afirmação foi geral demais, mas acho que o conselho de Danny é ainda mais generalizador e até perigoso
Marcus Müller
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.