Получение быстрой производительности от STM32 MCU


11

Я работаю с комплектом обнаружения STM32F303VC , и я немного озадачен его производительностью. Чтобы познакомиться с системой, я написал очень простую программу, просто чтобы проверить скорость этого битового микроконтроллера. Код можно разбить следующим образом:

  1. Часы HSI (8 МГц) включены;
  2. PLL запускается с прескалером 16 для достижения HSI / 2 * 16 = 64 МГц;
  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;

      }
}

Код был скомпилирован с CoIDE V2 с помощью встроенного набора инструментов GNU ARM с использованием оптимизации -O1. Сигналы на выводах PA8 (MCO) и PE10, проверенные осциллографом, выглядят так: введите описание изображения здесь

SYSCLK, кажется, настроен правильно, поскольку MCO (оранжевая кривая) демонстрирует колебания почти 64 МГц (с учетом погрешности внутреннего тактового сигнала). Странная часть для меня - это поведение на PE10 (синяя кривая). В бесконечном цикле while (1) требуется 4 + 4 + 5 = 13 тактовых циклов для выполнения элементарной трехэтапной операции (то есть установка битов / сброс битов / возврат). Это становится еще хуже на других уровнях оптимизации (например, -O2, -O3, ar -Os): несколько дополнительных тактовых циклов добавляются к НИЗКОЙ части сигнала, то есть между падающим и нарастающим фронтами PE10 (включение LSI как-то кажется чтобы исправить эту ситуацию).

Это поведение ожидается от этого MCU? Я хотел бы представить, что такая простая задача, как установка и сброс битов, должна быть в 2-4 раза быстрее. Есть ли способ ускорить процесс?


Вы пробовали сравнить с другим MCU?
Марко Буршич

3
Чего ты пытаешься достичь? Если вы хотите быстрый колебательный выход, вы должны использовать таймеры. Если вы хотите взаимодействовать с быстрыми последовательными протоколами, вы должны использовать соответствующее аппаратное периферийное оборудование.
Йонас Шефер

2
Отличное начало с комплектом!
Скотт Сейдман

Вы не должны | = BSRR или BRR регистры, так как они только для записи.
P__J__

Ответы:


25

Вопрос здесь действительно таков: что это за машинный код, который вы генерируете из программы на С, и чем он отличается от того, что вы ожидаете.

Если бы у вас не было доступа к исходному коду, это было бы упражнением в обратном инжиниринге (в основном это что-то, начинающееся с:) radare2 -A arm image.bin; aaa; VV, но у вас есть код, так что это все упрощает.

Сначала скомпилируйте его с -gдобавленным флагом (в том CFLAGSже месте, где вы также указали -O1). Затем посмотрите на сгенерированную сборку:

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

Обратите внимание, что, конечно, и имя objdumpдвоичного файла, и промежуточный файл ELF могут отличаться.

Обычно вы также можете просто пропустить часть, где GCC вызывает ассемблер, и просто посмотреть файл сборки. Просто добавьте -Sв командную строку GCC - но это, как правило, нарушит вашу сборку, так что вы, скорее всего, сделаете это вне вашей 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 в начале).

То, что мы видим здесь, это то, что мы

  • сначала ldr(регистр загрузки) регистр r2со значением в ячейке памяти, хранящимся в r3+ 24 байтах. Быть слишком ленивым, чтобы искать это: очень вероятно местоположение BSRR.
  • Тогда регистр с константой , которая соответствовала бы установить бит 10 в этом регистре, и записать результат на себя.ORr21024 == (1<<10)r2
  • Затем str(сохраните) результат в ячейке памяти, с которой мы прочитали на первом шаге
  • и затем повторите то же самое для другой области памяти, из ленивости: скорее всего BRR, адрес.
  • Наконец b(ответвление) вернуться к первому шагу.

Итак, у нас есть 7 инструкций, а не три, для начала. Только bодин раз происходит, и, таким образом, очень вероятно, что требуется нечетное количество циклов (у нас их всего 13, поэтому где-то должно появиться нечетное количество циклов). Поскольку все нечетные числа ниже 13 равны 1, 3, 5, 7, 9, 11, и мы можем исключить любые числа больше 13-6 (при условии, что ЦП не может выполнить инструкцию менее чем за один цикл), мы знаем что bтребуется 1, 3, 5 или 7 циклов ЦП.

Будучи тем, кто мы есть, я посмотрел на документацию ARM с инструкциями и сколько циклов они принимают для M3:

  • ldr занимает 2 цикла (в большинстве случаев)
  • orr занимает 1 цикл
  • str занимает 2 цикла
  • bзанимает от 2 до 4 циклов. Мы знаем, что это должно быть нечетное число, поэтому здесь должно быть 3.

Это все соответствует вашему наблюдению:

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

Как показывают вышеприведенные расчеты, вряд ли будет способ ускорить ваш цикл - выходные контакты на процессорах ARM обычно отображаются в память , а не в регистры ядра процессора, поэтому вам придется пройти обычную процедуру загрузки - изменения - сохранения, если Вы хотите сделать что-нибудь с ними.

Конечно, вы могли бы не читать ( |=неявно должен читать) значение вывода на каждой итерации цикла, а просто записывать в него значение локальной переменной, которое вы просто переключаете на каждой итерации цикла.

Обратите внимание, что я чувствую, что вы, возможно, знакомы с 8-битными микросхемами и пытаетесь прочитать только 8-битные значения, сохранить их в локальных 8-битных переменных и записать их в 8-битных порциях. Не. ARM - это 32-битная архитектура, и для извлечения 8-битного 32-битного слова могут потребоваться дополнительные инструкции. Если вы можете, просто прочитайте все 32-битное слово, измените то, что вам нужно, и запишите его как целое. Конечно, возможно ли это, зависит от того, к чему вы пишете, то есть от компоновки и функциональности вашего GPIO с отображением в памяти. Обратитесь к таблице данных / руководству пользователя STM32F3 за информацией о том, что хранится в 32-битном бите, содержащем бит, который вы хотите переключить.


Теперь я попытался воспроизвести вашу проблему с удлинением «низкого» периода, но я просто не смог - цикл выглядит точно так же, -O3как и -O1с моей версией компилятора. Вам придется сделать это самостоятельно! Возможно, вы используете какую-то древнюю версию GCC с неоптимальной поддержкой ARM.


4
Не будет ли , как вы говорите, просто хранить ( =вместо |=) именно то ускорение, которое ищет ОП? Причина, по которой ARM имеют регистры BRR и BSRR по отдельности, заключается в том, что они не требуют чтения-изменения-записи. В этом случае константы могут храниться в регистрах вне цикла, поэтому внутренний цикл будет состоять из двух строк и ветви, поэтому 2 + 2 +3 = 7 циклов для всего цикла?
Тимо

Спасибо. Это действительно немного прояснило ситуацию. Было немного поспешно думать, что нужно будет только 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
uxthесть, потому что GPIO->BSRRL(неправильно) определен как 16-битный регистр в ваших заголовках. Используйте последнюю версию заголовков из библиотек STM32CubeF3 , где нет BSRRL и BSRRH, но есть один 32-битный BSRRрегистр. @Marcus, очевидно, имеет правильные заголовки, поэтому его код делает полный 32-битный доступ вместо того, чтобы загружать полуслово и расширять его.
Беренди - протестуя

Почему загрузка одного байта требует дополнительных инструкций? В архитектуре ARM есть LDRBи STRBто, что выполняет чтение / запись байтов в одной инструкции, нет?
psmears

1
Ядро M3 может поддерживать полосу битов (не уверен, что эта конкретная реализация поддерживает), где область 1 МБ периферийного пространства памяти совмещена с областью 32 МБ. Каждый бит имеет адрес дискретного слова (используется только бит 0). Предположительно все еще медленнее, чем просто загрузка / хранение.
Шон

8

В BSRRи BRRрегистры для установки и сброса отдельных битов порта:

Регистр установки / сброса битов порта 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

Запустите его столько миллионов раз, сколько захотите, но у вас будет возможность варьировать производительность этого цикла, просто эти две инструкции, добавьте несколько штифтов в середине, если хотите; это не важно

Изменение выравнивания цикла может значительно изменить производительность, особенно с таким небольшим циклом, если вместо двух строк выборки требуется одна, вы тратите эти дополнительные затраты на такой микроконтроллер, где флэш-память медленнее ЦП на 2 или 3, а затем при увеличении тактовой частоты соотношение становится еще хуже 3 или 4 или 5, чем при добавлении дополнительной выборки.

Скорее всего, у вас нет кэша, но если он у вас есть, это помогает в некоторых случаях, но в других - и / или не имеет значения. Предсказание ветвления, которое вы можете или не можете иметь здесь (вероятно, нет), может видеть только в той степени, в которой оно спроектировано в канале, поэтому даже если вы изменили цикл на ветвление и имели безусловное ветвление в конце (проще для предиктора ветвления use) все, что делает, - это экономит столько часов (размер канала, откуда он обычно выбирает, насколько глубоко может видеть предиктор) при следующей выборке и / или он не выполняет предварительную выборку на всякий случай.

Изменяя выравнивание относительно строк выборки и кэша, вы можете повлиять на то, помогает ли вам предиктор ветвления или нет, и это можно увидеть в общей производительности, даже если вы тестируете только две инструкции или эти две с некоторыми nops ,

Это довольно тривиально, и как только вы поймете, что, взяв скомпилированный код или даже рукописную сборку, вы увидите, что его производительность может сильно различаться из-за этих факторов, добавляя или экономя от нескольких до нескольких сотен процентов, одна строка кода C, одна плохо размещенная nop.

После того, как вы научитесь использовать регистр BSRR, попробуйте запустить ваш код из оперативной памяти (копирование и переход) вместо флэш-памяти, что должно дать вам мгновенный прирост производительности в 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очень хорошо, и компиляторы ( ) могут делать это очень хорошо, и когда злоупотребление (как здесь) имеет обратный эффект от того, что вы хотите.
Маркус Мюллер

Бесконечный цикл никогда не может быть эффективно развернут для поддержания согласованного поведения времени.
Маркус Мюллер

1
@ MarcusMüller: бесконечные циклы иногда можно эффективно развернуть, сохраняя при этом согласованное время, если в некоторых повторениях цикла есть какие-либо точки, в которых инструкция не будет иметь видимого эффекта. Например, если somePortLatchуправляет портом, чьи младшие 4 бита установлены для вывода, может быть возможно развернуть while(1) { SomePortLatch ^= (ctr++); }код, который выводит 15 значений, а затем вернуться к началу, когда он в противном случае вывел бы одно и то же значение дважды в строке.
суперкат

Суперкат, правда. Кроме того, такие эффекты, как синхронизация интерфейса памяти и т. Д. Могут сделать целесообразным «частичное» развертывание. Мое утверждение было слишком общим, но я чувствую, что совет Дэнни является еще более обобщающим и даже опасным
Маркус Мюллер
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.