STM32 MCU에서 빠른 성능 얻기


11

STM32F303VC 디스커버리 키트로 작업 중이며 성능에 약간 당황합니다. 시스템에 익숙해지기 위해이 MCU의 비트 뱅킹 속도를 테스트하는 매우 간단한 프로그램을 작성했습니다. 코드는 다음과 같이 분류 할 수 있습니다.

  1. HSI 클럭 (8MHz)이 켜져 있습니다.
  2. PLL은 16의 프리스케일러로 시작하여 HSI / 2 * 16 = 64 MHz를 달성한다;
  3. PLL은 SYSCLK로 지정됩니다.
  4. SYSCLK는 MCO 핀 (PA8)에서 모니터링되며 핀 중 하나 (PE10)는 무한 루프에서 지속적으로 토글됩니다.

이 프로그램의 소스 코드는 다음과 같습니다.

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

      }
}

코드는 -O1 최적화를 사용하여 GNU ARM 임베디드 툴체인과 함께 CoIDE V2로 컴파일되었습니다. 오실로스코프로 검사 한 핀 PA8 (MCO) 및 PE10의 신호는 다음과 같습니다. 여기에 이미지 설명을 입력하십시오

MCO (주황색 곡선)가 거의 64MHz의 진동 (내부 클록의 오류 마진을 고려한)을 나타 내기 때문에 SYSCLK가 올바르게 구성된 것처럼 보입니다. 나에게 이상한 부분은 PE10 (파란색 곡선)의 동작입니다. 무한 while (1) 루프에서는 기본 3 단계 작업 (즉, 비트 세트 / 비트 리셋 / 리턴)을 수행하는 데 4 + 4 + 5 = 13 클록 사이클이 필요합니다. 다른 최적화 레벨 (예 : -O2, -O3, ar -Os)에서는 더욱 악화됩니다. 신호의 LOW 부분에 몇 가지 추가 클록 사이클이 추가됩니다 (예 : PE10의 하강 및 상승 에지 사이). 이 상황을 해결하기 위해).

이 동작은이 MCU에서 예상됩니까? 비트를 설정하고 재설정하는 것만 큼 간단한 작업을 2-4 배 더 빨라야한다고 생각합니다. 속도를 높일 수있는 방법이 있습니까?


다른 MCU와 비교해 보셨습니까?
Marko Buršič

3
무엇을 달성하려고합니까? 빠른 발진 출력을 원하면 타이머를 사용해야합니다. 빠른 직렬 프로토콜과 인터페이스하려면 해당 하드웨어 주변 장치를 사용해야합니다.
Jonas Schäfer

2
키트로 시작하세요!
Scott Seidman

BSRR 또는 BRR 레지스터는 쓰기 전용이므로 | =해서는 안됩니다.
P__J__

답변:


25

여기서 질문은 실제로 C 프로그램에서 생성하는 기계 코드는 무엇이며 예상과 어떻게 다른가입니다.

원래 코드에 액세스 할 수 없다면 리버스 엔지니어링 (기본적으로 :으로 시작하는 것)의 연습 radare2 -A arm image.bin; aaa; VV이었지만 코드를 얻었으므로 더 쉬워졌습니다.

먼저, -g플래그를 추가 한 상태로 컴파일 하십시오 CFLAGS(또한 같은 위치에 지정 -O1). 그런 다음 생성 된 어셈블리를보십시오.

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

물론 objdump바이너리 의 이름과 중간 ELF 파일이 다를 수 있습니다.

일반적으로 GCC가 어셈블러를 호출하는 부분을 건너 뛰고 어셈블리 파일을 볼 수도 있습니다. -SGCC 명령 줄에 추가하면 됩니다.하지만 일반적으로 빌드가 중단되므로 IDE 외부에서 수행하는 것이 좋습니다.

나는 한 코드의 약간 패치 버전의 어셈블리 :

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

다음을 얻었습니다 (위 링크에서 발췌 한 전체 코드).

.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

이것은 루프입니다 (무조건 점프는 끝에서 .L5로, 시작에서 .L5 레이블로 이동합니다).

우리가 여기서 보는 것은

  • +24 바이트에 저장된 메모리 위치에 값이 ldr있는 레지스터 r2를 먼저 (로드 레지스터) 등록 하십시오 r3. 찾아보기에는 너무 게으르다 :의 위치 일 가능성이 높다 BSRR.
  • 그런 다음 상수가 OR있는 r2레지스터 1024 == (1<<10)는 해당 레지스터의 10 번째 비트를 설정하고 해당 결과를 r2자체에 씁니다 .
  • 그런 다음 str첫 번째 단계에서 읽은 메모리 위치에 결과를 저장하십시오.
  • 그리고 게으름에서 다른 메모리 위치에 대해서도 동일하게 반복하십시오 BRR.
  • 마지막으로 b첫 번째 단계로 돌아갑니다.

세 가지가 아닌 7 가지 지침이 있습니다. 오직이 b매우 높다 사이클의 홀수를 복용 무엇 때문에 한 번 발생하고, (우리는 그렇게 어딘가 이상한 사이클 카운트에서 온해야합니다 총 13있다). 13 미만의 모든 홀수는 1, 3, 5, 7, 9, 11이며 13-6보다 큰 숫자는 배제 할 수 있기 때문에 (CPU 가 한 사이클 미만 으로 명령을 실행할 수 없다고 가정 ) (가) 있음 b1, 3, 5, 7 CPU 사이클이 걸린다.

우리가 누구인지에 대해 ARM의 명령어 설명서와 M3에 소요 되는 주기를 살펴 보았습니다 .

  • ldr 2주기 소요 (대부분의 경우)
  • orr 1주기 소요
  • str 2주기 소요
  • b2 ~ 4주기가 걸립니다. 우리는 그것이 홀수 여야한다는 것을 알고 있으므로 여기서 3 이 필요 합니다 .

그것은 모두 당신의 관찰과 일치합니다 :

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

위의 계산에서 알 수 있듯이 루프를 더 빠르게 만드는 방법은 거의 없습니다. ARM 프로세서의 출력 핀은 일반적으로 CPU 코어 레지스터가 아닌 메모리 매핑 되므로 일반적인 로드 – 수정 – 저장 루틴을 수행해야합니다. 그들과 함께하고 싶은 일이 있습니다.

당신은 물론 읽을 수 있습니다 무엇을 할 수 있는지 ( |=암시 적으로 핀의 값마다 루프 반복을하지만, 단지 어떤 방금 토글마다 루프 반복, 그것을 지역 변수의 값을 쓰기 읽기).

8 비트 마이크로에 익숙하고 8 비트 값만 읽고 로컬 8 비트 변수에 저장하고 8 비트 청크에 쓰려고한다고 생각합니다. 하지마 ARM은 32 비트 아키텍처이므로 32 비트 워드의 8 비트를 추출하려면 추가 지침이 필요할 수 있습니다. 가능하다면 32 비트 단어 전체를 읽고 필요한 것을 수정 한 다음 전체 단어로 다시 쓰십시오. 이것이 가능한지 여부는 여러분이 쓰고있는 것, 즉 메모리 매핑 GPIO의 레이아웃과 기능에 달려 있습니다. 토글하려는 비트가 포함 된 32 비트에 저장된 내용에 대한 정보는 STM32F3 데이터 시트 / 사용자 안내서를 참조하십시오.


이제는 "낮음"기간이 길어지면서 문제를 재현하려고 시도했지만 루프는 컴파일러 버전에서 -O3와 정확히 똑같이 보입니다 -O1. 당신은 그것을 직접해야합니다! 차선책으로 ARM을 지원하는 고대 버전의 GCC를 사용하고있을 수 있습니다.


4
OP가 찾고있는 속도를 정확히 높이는 것만으로 ( =대신 대신 |=) 저장하지 않습니까? ARM에 BRR 및 BSRR 레지스터가 별도로있는 이유는 읽기-수정-쓰기가 필요하지 않기 때문입니다. 이 경우 상수는 루프 외부의 레지스터에 저장할 수 있으므로 내부 루프는 2 str과 분기이므로 전체 라운드에 대해 2 + 2 +3 = 7 사이클입니까?
Timo

감사. 그것은 정말로 상황을 약간 정리했습니다. 3 클럭 사이클 만 필요하다고 주장하는 것은 약간의 성급한 생각이었습니다. 6 ~ 7 사이클은 제가 실제로 기대했던 것입니다. -O3오류 청소 및 솔루션을 재건 한 후 사라진 것 같습니다. 그럼에도 불구하고 내 어셈블리 코드에는 추가 UTXH 명령어가있는 것 같습니다. .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
uxthGPIO->BSRRL헤더에 16 비트 레지스터로 잘못 정의되어 있기 때문 입니다. BSRRL 및 BSRRH가없고 단일 32 비트 레지스터 가 있는 STM32CubeF3 라이브러리 에서 최신 버전의 헤더를 사용하십시오 BSRR. @Marcus는 분명히 올바른 헤더를 가지고 있으므로 그의 코드는 하프 워드를로드하고 확장하는 대신 전체 32 비트 액세스를 수행합니다.
berendi-시위

단일 바이트를로드하는 데 추가 명령이 필요한 이유는 무엇입니까? ARM 아키텍처가 가지고 LDRBSTRB바이트 아니, 단일 명령에 / 쓰기를 읽 수행?
psmears

1
M3 코어 주변 메모리 공간의 1MB 영역이 32MB 영역으로 별칭이 지정된 비트 밴딩 (이 특정 구현이 확실하지 않은지 여부)을 지원할 수 있습니다 . 각 비트에는 이산 워드 주소가 있습니다 (비트 0 만 사용). 아마도로드 / 스토어보다 여전히 느릴 것입니다.
Sean Houlihane

8

BSRRBRR레지스터는 각각의 포트의 비트를 설정 및 재설정이다 :

GPIO 포트 비트 설정 / 리셋 레지스터 (GPIOx_BSRR)

...

(x = A..H) 비트 15 : 0

BSy : 포트 x 세트 비트 y (y = 0..15)

이 비트는 쓰기 전용입니다. 이 비트를 읽으면 0x0000 값이 반환됩니다.

0 : 해당 ODRx 비트에 대한 동작 없음

1 : 해당 ODRx 비트를 설정합니다

보시다시피, 이러한 레지스터를 읽으면 항상 0이되므로 코드는 무엇입니까?

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

효과적으로 수행이다 GPIOE->BRR = 0 | GPIO_BRR_BR_10, 그러나의 시퀀스를 생성하도록 최적화, 알고하지 않습니다 LDR, ORR, STR대신 단일 저장소의 지침을 제공합니다.

간단히 쓰기 만하면 값 비싼 읽기-수정-쓰기 작업을 피할 수 있습니다

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

루프를 8로 균등하게 나눌 수있는 루프에 맞추면 더 개선 될 수 있습니다 . 루프 asm("nop");앞에 하나 또는 모드 명령을 입력하십시오 while(1).


1

여기에 언급 된 내용을 추가하려면 Cortex-M을 사용하지만 거의 모든 프로세서 (파이프 라인, 캐시, 분기 예측 또는 기타 기능 포함)를 사용하면 가장 간단한 루프조차 사용하는 것이 쉽지 않습니다.

top:
   subs r0,#1
   bne top

원하는만큼 수백만 번 실행하지만 루프의 성능이 크게 달라 지도록 할 수 있어야합니다.이 두 명령 만 원한다면 중간에 nops를 추가하십시오. 중요하지 않습니다.

루프의 정렬을 변경하면 성능이 크게 달라질 수 있습니다. 특히 작은 루프의 경우 하나 대신 두 개의 인출 라인이 필요한 경우 플래시가 CPU보다 2 배 느린 플래시와 같은 마이크로 컨트롤러에서 추가 비용을 소비합니다. 또는 3을 누른 다음 시계를 올리면 추가 페칭을 추가하는 것보다 비율이 3 또는 4 또는 5가 더 나빠집니다.

캐시가 없을 수도 있지만 캐시가 있으면 도움이되지만 다른 경우에는 아프거나 차이를 만들지 않습니다. 여기에 있거나 없을 수있는 분기 예측은 파이프에서 설계된 한까지만 볼 수 있으므로 루프를 분기로 변경하고 끝에 무조건 분기가있는 경우에도 (분기 예측자가 쉽게 할 수 있음) 사용)하면 다음 페치에서 많은 클록 (일반적으로 페치하는 지점에서 예측자가 볼 수있는 깊이까지 파이프의 크기)을 저장하고 /하거나 경우에 대비하여 프리 페치를 수행하지 않습니다.

페치 및 캐시 라인과 관련하여 정렬을 변경하면 분기 예측 변수가 도움이되는지 여부에 영향을 줄 수 있으며 두 명령 만 테스트하거나 일부 nops를 사용하는 경우에도 전체 성능에서 볼 수 있습니다 .

이 작업을 수행하는 것은 다소 사소한 일이며, 일단 이해 한 다음 컴파일 된 코드 또는 수작업으로 작성한 어셈블리를 이해하면 이러한 요소로 인해 성능이 크게 달라질 수 있음을 알 수 있습니다. 한 줄의 C 코드, 한 줄이 잘못 배치 된 nop.

BSRR 레지스터 사용을 학습 한 후에는 플래시 대신 RAM (복사 및 점프)에서 코드를 실행하여 다른 작업을 수행하지 않고도 실행 속도를 2 ~ 3 배 향상시킬 수 있습니다.


0

이 동작은이 MCU에서 예상됩니까?

코드의 동작입니다.

  1. 지금처럼 읽기-수정-쓰기가 아닌 BRR / BSRR 레지스터에 기록해야합니다.

  2. 루프 오버 헤드도 발생합니다. 성능을 최대화하려면 BRR / BSRR 조작을 반복해서 반복하고 → 루프에 여러 번 복사하여 붙여 넣기하여 한 번의 루프 오버 헤드 전에 여러 설정 / 리셋주기를 수행하십시오.

편집 : IAR에서 몇 가지 빠른 테스트.

BRR / BSRR에 쓰기를 통한 전환은 중간 수준의 최적화에서 6 개의 명령어를 사용하고 최고 수준의 최적화에서 3 개의 명령어를 사용합니다. RMW'ng을 통한 플립은 10 명령어 / 6 명령어를 취합니다.

루프 오버 헤드 추가.


변경 |==단일 비트 세트 / 리셋 위상은 9 클럭 사이클 (소비 링크 ). 조립 코드는 3 명령어 길이입니다 :.L5 strh r1, [r3, #24] @ movhi str r2, [r3, #40] b .L5
KR

1
루프를 수동으로 풀지 마십시오 . 그것은 결코 좋은 생각이 아닙니다. 이 특별한 경우에는 특히 비참합니다. 파형을 비 주기적으로 만듭니다. 또한 플래시에서 동일한 코드를 여러 번 보유하는 것이 반드시 빠를 필요는 없습니다. 이것은 여기에 적용되지 않을 수도 있지만 (그럴 수도 있습니다!) 루프 언 롤링은 많은 사람들이 도움을주고 컴파일러 ( gcc -funroll-loops)가 매우 잘 수행 할 수 있으며 악용 될 때 (여기에서와 같이) 원하는 것의 역효과를 낳습니다.
Marcus Müller

일정한 타이밍 동작을 유지하기 위해 무한 루프 를 효과적으로 풀 수 없습니다 .
마커스 ül 러

1
@ MarcusMüller : 명령이 눈에 띄지 않는 루프의 일부 반복에 포인트가있는 경우 일정한 루프를 유지하면서 무한 루프가 유용하게 풀릴 수 있습니다. 예를 들어, somePortLatch하위 4 비트가 출력용으로 설정된 포트를 제어하는 경우 while(1) { SomePortLatch ^= (ctr++); }15 개의 값을 출력하는 코드 로 언롤 한 다음 동일한 값을 두 번 연속으로 출력 할 때 시작하도록 루프백 할 수 있습니다.
supercat

슈퍼 캣, 사실. 또한 메모리 인터페이스 타이밍 등과 같은 효과로 인해 "부분적으로"풀리는 것이 합리적 일 수 있습니다. 제 말은 너무 일반적 이었지만, Danny의 조언이 더욱 일반화되고 심지어 위험하다고 생각합니다
Marcus Müller
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.