Я работаю с комплектом обнаружения STM32F303VC , и я немного озадачен его производительностью. Чтобы познакомиться с системой, я написал очень простую программу, просто чтобы проверить скорость этого битового микроконтроллера. Код можно разбить следующим образом:
- Часы HSI (8 МГц) включены;
- PLL запускается с прескалером 16 для достижения HSI / 2 * 16 = 64 МГц;
- PLL обозначается как SYSCLK;
- 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 раза быстрее. Есть ли способ ускорить процесс?
Ответы:
Вопрос здесь действительно таков: что это за машинный код, который вы генерируете из программы на С, и чем он отличается от того, что вы ожидаете.
Если бы у вас не было доступа к исходному коду, это было бы упражнением в обратном инжиниринге (в основном это что-то, начинающееся с:)
radare2 -A arm image.bin; aaa; VV
, но у вас есть код, так что это все упрощает.Сначала скомпилируйте его с
-g
добавленным флагом (в томCFLAGS
же месте, где вы также указали-O1
). Затем посмотрите на сгенерированную сборку:Обратите внимание, что, конечно, и имя
objdump
двоичного файла, и промежуточный файл ELF могут отличаться.Обычно вы также можете просто пропустить часть, где GCC вызывает ассемблер, и просто посмотреть файл сборки. Просто добавьте
-S
в командную строку GCC - но это, как правило, нарушит вашу сборку, так что вы, скорее всего, сделаете это вне вашей IDE.Я сделал сборку слегка исправленной версии вашего кода :
и получил следующее (выдержка, полный код по ссылке выше):
Это цикл (обратите внимание на безусловный переход к .L5 в конце и метке .L5 в начале).
То, что мы видим здесь, это то, что мы
ldr
(регистр загрузки) регистрr2
со значением в ячейке памяти, хранящимся вr3
+ 24 байтах. Быть слишком ленивым, чтобы искать это: очень вероятно местоположениеBSRR
.OR
r2
1024 == (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.Это все соответствует вашему наблюдению:
Как показывают вышеприведенные расчеты, вряд ли будет способ ускорить ваш цикл - выходные контакты на процессорах ARM обычно отображаются в память , а не в регистры ядра процессора, поэтому вам придется пройти обычную процедуру загрузки - изменения - сохранения, если Вы хотите сделать что-нибудь с ними.
Конечно, вы могли бы не читать (
|=
неявно должен читать) значение вывода на каждой итерации цикла, а просто записывать в него значение локальной переменной, которое вы просто переключаете на каждой итерации цикла.Обратите внимание, что я чувствую, что вы, возможно, знакомы с 8-битными микросхемами и пытаетесь прочитать только 8-битные значения, сохранить их в локальных 8-битных переменных и записать их в 8-битных порциях. Не. ARM - это 32-битная архитектура, и для извлечения 8-битного 32-битного слова могут потребоваться дополнительные инструкции. Если вы можете, просто прочитайте все 32-битное слово, измените то, что вам нужно, и запишите его как целое. Конечно, возможно ли это, зависит от того, к чему вы пишете, то есть от компоновки и функциональности вашего GPIO с отображением в памяти. Обратитесь к таблице данных / руководству пользователя STM32F3 за информацией о том, что хранится в 32-битном бите, содержащем бит, который вы хотите переключить.
Теперь я попытался воспроизвести вашу проблему с удлинением «низкого» периода, но я просто не смог - цикл выглядит точно так же,
-O3
как и-O1
с моей версией компилятора. Вам придется сделать это самостоятельно! Возможно, вы используете какую-то древнюю версию GCC с неоптимальной поддержкой ARM.источник
=
вместо|=
) именно то ускорение, которое ищет ОП? Причина, по которой ARM имеют регистры BRR и BSRR по отдельности, заключается в том, что они не требуют чтения-изменения-записи. В этом случае константы могут храниться в регистрах вне цикла, поэтому внутренний цикл будет состоять из двух строк и ветви, поэтому 2 + 2 +3 = 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
uxth
есть, потому чтоGPIO->BSRRL
(неправильно) определен как 16-битный регистр в ваших заголовках. Используйте последнюю версию заголовков из библиотек STM32CubeF3 , где нет BSRRL и BSRRH, но есть один 32-битныйBSRR
регистр. @Marcus, очевидно, имеет правильные заголовки, поэтому его код делает полный 32-битный доступ вместо того, чтобы загружать полуслово и расширять его.LDRB
иSTRB
то, что выполняет чтение / запись байтов в одной инструкции, нет?В
BSRR
иBRR
регистры для установки и сброса отдельных битов порта:Как видите, чтение этих регистров всегда дает 0, следовательно, каков ваш код
делает эффективно это
GPIOE->BRR = 0 | GPIO_BRR_BR_10
, но оптимизатор не знает , что, таким образом он генерирует последовательностьLDR
,ORR
,STR
инструкции вместо одного магазина.Вы можете избежать дорогостоящей операции чтения-изменения-записи, просто написав
Вы можете получить дальнейшее улучшение, выровняв цикл по адресу, равномерно делимому на 8. Попробуйте поместить одну из
asm("nop");
инструкций или инструкции режима передwhile(1)
циклом.источник
Чтобы добавить к сказанному здесь: конечно, с Cortex-M, но почти с любым процессором (с конвейером, кешем, предсказанием ветвлений или другими функциями), тривиально выполнить даже самый простой цикл:
Запустите его столько миллионов раз, сколько захотите, но у вас будет возможность варьировать производительность этого цикла, просто эти две инструкции, добавьте несколько штифтов в середине, если хотите; это не важно
Изменение выравнивания цикла может значительно изменить производительность, особенно с таким небольшим циклом, если вместо двух строк выборки требуется одна, вы тратите эти дополнительные затраты на такой микроконтроллер, где флэш-память медленнее ЦП на 2 или 3, а затем при увеличении тактовой частоты соотношение становится еще хуже 3 или 4 или 5, чем при добавлении дополнительной выборки.
Скорее всего, у вас нет кэша, но если он у вас есть, это помогает в некоторых случаях, но в других - и / или не имеет значения. Предсказание ветвления, которое вы можете или не можете иметь здесь (вероятно, нет), может видеть только в той степени, в которой оно спроектировано в канале, поэтому даже если вы изменили цикл на ветвление и имели безусловное ветвление в конце (проще для предиктора ветвления use) все, что делает, - это экономит столько часов (размер канала, откуда он обычно выбирает, насколько глубоко может видеть предиктор) при следующей выборке и / или он не выполняет предварительную выборку на всякий случай.
Изменяя выравнивание относительно строк выборки и кэша, вы можете повлиять на то, помогает ли вам предиктор ветвления или нет, и это можно увидеть в общей производительности, даже если вы тестируете только две инструкции или эти две с некоторыми nops ,
Это довольно тривиально, и как только вы поймете, что, взяв скомпилированный код или даже рукописную сборку, вы увидите, что его производительность может сильно различаться из-за этих факторов, добавляя или экономя от нескольких до нескольких сотен процентов, одна строка кода C, одна плохо размещенная nop.
После того, как вы научитесь использовать регистр BSRR, попробуйте запустить ваш код из оперативной памяти (копирование и переход) вместо флэш-памяти, что должно дать вам мгновенный прирост производительности в 2-3 раза, не делая ничего другого.
источник
Это поведение вашего кода.
Вы должны записывать в регистры BRR / BSRR, а не читать-модифицировать-записывать, как сейчас.
Вы также несете накладные расходы петли. Для достижения максимальной производительности повторяйте операции 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
gcc -funroll-loops
очень хорошо, и компиляторы ( ) могут делать это очень хорошо, и когда злоупотребление (как здесь) имеет обратный эффект от того, что вы хотите.somePortLatch
управляет портом, чьи младшие 4 бита установлены для вывода, может быть возможно развернутьwhile(1) { SomePortLatch ^= (ctr++); }
код, который выводит 15 значений, а затем вернуться к началу, когда он в противном случае вывел бы одно и то же значение дважды в строке.