Отримання швидкої продуктивності від MCM STM32


11

Я працюю з набором для виявлення STM32F303VC, і я трохи спантеличений його роботою. Для ознайомлення з системою я написав дуже просту програму, щоб просто перевірити швидкість удару бітів цього MCU. Код можна розділити наступним чином:

  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 налаштований правильно, оскільки МКО (помаранчева крива) має коливання майже 64 МГц (з огляду на похибку внутрішнього тактового сигналу). Дивна для мене частина - поведінка на PE10 (синя крива). У нескінченному циклі (1) потрібно 4 + 4 + 5 = 13 тактових циклів для виконання елементарної 3-ступінкової операції (тобто біт-набір / біт-скидання / повернення). Це стає ще гіршим на інших рівнях оптимізації (наприклад, -O2, -O3, ar -Os): кілька додаткових тактових циклів додаються до низької частини сигналу, тобто між падаючими та піднімаючими краями PE10 (що дозволяє LSI якось здається виправити цю ситуацію).

Чи очікується така поведінка від цього MCU? Я б уявив, що завдання таке просте, як встановлення та скидання трохи повинно бути в 2-4 рази швидше. Чи є спосіб пришвидшити справи?


Ви пробували з іншими MCU для порівняння?
Марко Буршич

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

2
Чудовий початок з комплекту !!
Скотт Сейдман

Ви не повинні | = регістри 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 викликає асемблер, і просто подивитися файл складання. Просто додайте -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-бітове слово, модифікуйте те, що вам потрібно, і запишіть його як ціле. Чи можливо це можливо, звичайно, залежить від того, що ви пишете, тобто компонування та функціональності вашого графічного відеореєстратора. Перегляньте таблицю даних / посібник користувача STM32F3 для отримання інформації про те, що зберігається в 32-бітовому біті, що містить біт, який ви хочете переключити.


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


4
Чи не просто зберігання ( =замість |=), як ви кажете, було б саме тим прискоренням, яке шукає ОП? Причина, що в ARM є окремі регістри BRR та BSRR, полягає в тому, що не потрібно читати-змінювати-записувати. У цьому випадку константи можна зберігати в регістрах за межами циклу, тож внутрішня петля була б лише 2 ст і гілка, тож 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-бітний доступ замість завантаження півслова та розширення.
berendi - протестуючи

Чому для завантаження одного байта потрібні додаткові інструкції? Архітектура 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, ніж додавання додаткового вибору.

Ви, ймовірно, не маєте кеш-пам'ять, але якщо у вас це було, що це допомагає в деяких випадках, але в інших це боляче і / або не має значення. Передбачення гілки, яке у вас може бути, а може і не бути (можливо, немає), може бачити лише те, наскільки розроблено в трубі, так що навіть якщо ви змінили цикл на гілку та мали безумовну гілку в кінці (легше для прогнозованого гілки використання) все, що робить, це заощадить вам, що багато годин (розмір труби, звідки це зазвичай можна отримати, наскільки глибоко може побачити передбачувач) на наступний вибор та / або він не робить попередній вибір на всякий випадок.

Змінивши вирівнювання щодо рядків вилучення та кешу, ви можете вплинути на те, чи допомагає вам передбачувач гілок чи ні, і це можна побачити в загальній продуктивності, навіть якщо ви протестуєте лише дві інструкції чи ті дві з деякими нопами .

Це зробити дещо тривіально, і як тільки ви зрозумієте, що, взявши складений код або навіть рукописний збір, ви зможете побачити, що його ефективність може сильно відрізнятися за рахунок цих факторів, додаючи або заощаджуючи від кількох до пари сотень відсотків, один рядок коду С, один погано розміщений nop.

Навчившись використовувати реєстр BSRR, спробуйте запустити свій код з оперативної пам’яті (скопіювати та перейти) замість спалаху, який повинен дати вам миттєве збільшення продуктивності у 2–3 рази, не роблячи нічого іншого.


0

Чи очікується така поведінка від цього MCU?

Це поведінка вашого коду.

  1. Ви повинні писати в регістри BRR / BSRR, а не читати-змінювати-писати, як зараз.

  2. Ви також несете петлю над головою. Для досягнення максимальної продуктивності повторюйте операції BRR / BSRR знову і знову → копіюйте та вставляйте їх у цикл кілька разів, щоб ви пройшли багато циклів встановлення / скидання перед накладенням одного циклу.

редагувати: кілька швидких тестів за IAR.

перегортання запису до BRR / BSRR приймає 6 інструкцій при помірній оптимізації та 3 інструкції при найвищому рівні оптимізації; перегортання RMW'ng займає 10 інструкцій / 6 інструкцій.

петля накладні додаткові.


Змінившись |=на =один набір бітів / фазу скидання, витрачається 9 тактових циклів ( посилання ). Код складання - три інструкції:.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 значень, а потім петлювати назад, щоб почати в той момент, коли в іншому випадку буде виводитися одне і те ж значення двічі поспіль.
supercat

Supercat, правда. Крім того, такі ефекти, як синхронізація інтерфейсу пам'яті тощо, можуть зробити його частково "частково" розкрученим. Моя заява була занадто загальною, але я вважаю, що порада Денні ще більш узагальнююча, і навіть небезпечніше
Маркус Мюллер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.