LINUX.ORG.RU

Скорость работы пина зависит от других пинов. Это нормально?

 bluepill, cmsis, ,


0

2

Привет, народ!

Заметил такую странность, которую не знаю как объяснить. Плата STM32F103C8T6 (Blue Pill)

Я хотел сделать мигание светодиодом на ноге A0 при инициализации контроллера на 72MHz.

Взял сделанный ранее проект и стал его упрощать. И вот когда оставил в проекте, по-сути, только:

- инициализацию на 72Mhz
- включение тактирования портов
- настройку пина A0,

то заметил, что код мигания стал работать медленнее! Т. е. мигание, сделанное в бесконечном цикле, стало в 1.5-2 раза медленнее, чем было до.

Я стал разбираться, что могло на это повлиять. И вернул вызов ненужной функции, в которой инициализировались пины A8, A9, B3, B4, B6, B7. И о чудо, мигание стало опять быстрым! Повторюсь, в этой функции делается только инициализация пинов, и она вызывается один раз в начале программы, ничего более.

Вот полный код: https://pastebin.com/Z7d0LZif

А вот код функции, которая «разгоняет» выполнение кода:

// Настройка пинов A8, A9, B3, B4, B6, B7
void otherPortInit(void)
{
    // Для начала сброс конфигурации всех используемых портов в ноль
    GPIOA->CRH &= ~(GPIO_CRH_MODE8 | GPIO_CRH_CNF8);
    GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9);

    GPIOB->CRL &= ~(GPIO_CRL_MODE3 | GPIO_CRL_CNF3);
    GPIOB->CRL &= ~(GPIO_CRL_MODE4 | GPIO_CRL_CNF4);
    GPIOB->CRL &= ~(GPIO_CRL_MODE6 | GPIO_CRL_CNF6);
    GPIOB->CRL &= ~(GPIO_CRL_MODE7 | GPIO_CRL_CNF7);


    uint32_t mode;
    uint32_t cnf;

    mode=0b11; // Режим выхода, с максимальной частотой 50 МГц
    cnf=0b00;  // Режим push-pull
    GPIOA->CRH |= (mode << GPIO_CRH_MODE8_Pos) | (cnf << GPIO_CRH_CNF8_Pos);
    GPIOA->CRH |= (mode << GPIO_CRH_MODE9_Pos) | (cnf << GPIO_CRH_CNF9_Pos);

    mode=0b00; // Режим входа
    cnf=0b01;  // Режим плавающего входа, подтяжки нет
    GPIOB->CRL |= (mode << GPIO_CRL_MODE3_Pos) | (cnf << GPIO_CRL_CNF3_Pos);
    GPIOB->CRL |= (mode << GPIO_CRL_MODE4_Pos) | (cnf << GPIO_CRL_CNF4_Pos);
    GPIOB->CRL |= (mode << GPIO_CRL_MODE6_Pos) | (cnf << GPIO_CRL_CNF6_Pos);
    GPIOB->CRL |= (mode << GPIO_CRL_MODE7_Pos) | (cnf << GPIO_CRL_CNF7_Pos);
}

Я не могу эту вещь объяснить. Почему настройки пинов, которые не используются в коде, так странно влияют на скорость выполнения программы контроллером? Мало того, в базовом проекте, на точно таком же коде я обнаружил обратный эффект: вызов этой функции инициализации портов замедляет мигание, а комментирование ее вызова - ускоряет.

В общем, я в недоумении. Я вообще не ожидал, что такое поведение возможно. Это тормозит разработку домашнего проекта, потому то в нем критична реакция на сигналы длительностью ~500нс, и тут я вижу, что тупой бесконечный цикл работает с разной скоростью в зависимости от инициализации неиспользуемых портов.

Вопрос 1: Как единственный вызов этой функции может влиять на скорость выполнения основного цикла?
Вопрос 2: Почему вызов этой функции может давать строго обратный эффект?

★★★★★

Типичная причина таких проблем - выравнивание кода и низкоуровневая архитектура памяти.

Если цикл весь поместился в границы буферов, то работает быстро. Если промазал - медленно.

Попробуй разное число от (1 до 16) asm(«nop»); вставить перед циклом и посмотри на результат.

alexru ★★★★
()
Ответ на: комментарий от alexru

Не факт. С одной стороны флешка напрямую не работает на такой частоте, только через буфер. Но с другой стороны это Cortex-M3, а там условно «гарвардская» архитектура, с тремя шинами System bus, Icode, Dcode. И нужно еще разбираться как будет быстрее.

В целом идея с софтовым подсчетом наносекундных задержек на Cortex-M3 как-то не очень.

DSSS
()
Ответ на: комментарий от DSSS

Разбираться в этом - это моя работа. Это очень типичная ситуация и проверить тривиально как я описал. Шины самого ядра тут мало на что влияют, важна организация флеша.

alexru ★★★★
()
Ответ на: комментарий от alexru

Открываем reference manual и смотрим самую первую картинку. Конечно же «Bus matrix» это простое комбинационное устройство и во всех случаях не добавит задержки.

Это очень типичная ситуация и проверить тривиально как я описал.

Опять смотрим мануал, раздел «Embedded Flash memory».

Read interface with prefetch buffer (2x64-bit words)

И сама флешка 64 бит. Откуда тогда взялось до 16 nop? Один nop это два байта.

DSSS
()
Ответ на: комментарий от DSSS

Откуда тогда взялось до 16 nop?

От лени размышлять когда попробовать занимает 5 минут. Естественно 16 - это больше, чем нужно на практике.

alexru ★★★★
()
Ответ на: комментарий от DSSS

Утверждают что для F103 однозначно будет быстрее из флешки.

Утверждают не верно. Но с обработчиками прерываний все не так просто, так как саму таблицу тоже нужно размещать в ОЗУ для полноценной оптимизации.

128 бит выборка покрывает тормоза на линейных участках, и вызов прерывания им не является. И обычные программы тоже в общем случает не являются. Поэтому и тормозит если цикл не попал целиком в буфер.

alexru ★★★★
()
Ответ на: комментарий от alexru

Типичная причина таких проблем - выравнивание кода и низкоуровневая архитектура памяти.

Поддерживаю. Для проверки проще вынести рабочий цикл в отдельную функцию, пометить её как __attribute__((noinline)), и поиграться с её выравниванием.

Beewek ★★★
()
Ответ на: комментарий от alexru

Мне тут сказали, что может оптимизация -Os в этих двух случаях по-разному срабатывает (хотя опции одинаковые для обеих вариантов, естественно). Типа msDelay() или компилится по-разному или в одном случае инлайнится, в другом - нет.

Но я дизассемблер посмотрел - ничего такого нет.

https://i.ibb.co/KsnrfWx/2022-05-03-23-03-26.png

В обеих случаях msDelay() компилится байт в байт, только смещение самой функции немного отличается. И в функции main() виден вызов топикстартовой инициализации otherPortInit() в начале. Все, больше отличий нет.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

msDelay() компилится байт в байт, только смещение самой функции немного отличается.

Вот это и есть главное. Флеш читается по 128 бит, и при смене смещения изменяется число кусков, которые надо вычитывать. Попробуй дать __attribute__ (( aligned(8) )) функции msDelay()

Beewek ★★★
()
Последнее исправление: Beewek (всего исправлений: 2)
Ответ на: комментарий от Xintrea

Пропустил. Это херня полная.

Если что, нормальный код для задержек:

__attribute__((noinline, section(".ramfunc")))
static void delay_ms(int ms)
{
  uint32_t cycles = ms * F_CPU / 3 / 1000;

  asm volatile (
    "1: subs %[cycles], %[cycles], #1 \n"
    "   bne 1b \n"
    : [cycles] "+r"(cycles)
  );
}

__attribute__((noinline, section(".ramfunc")))
void delay_cycles(uint32_t cycles)
{
  cycles /= 4;

  asm volatile (
    "1: subs %[cycles], %[cycles], #1 \n"
    "   nop \n"
    "   bne 1b \n"
    : [cycles] "+l"(cycles)
  );
}

alexru ★★★★
()
Последнее исправление: alexru (всего исправлений: 1)
Ответ на: комментарий от Beewek

Попробуй дать __attribute__ (( aligned(8) )) функции msDelay()

Да, это надо проверить. Вечером доберусь до оборудования и попробую.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Да, это надо проверить. Вечером доберусь до оборудования и попробую.

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

alexru ★★★★
()
Ответ на: комментарий от Xintrea

Что-то там у тебя лютое. Задержки если и есть, то глазами точно не должны быть видны.

ox55ff ★★★★★
()
Ответ на: комментарий от alexru

Важно выравнивание внутреннего цикла, а не всей функции.

Ну тогда основной цикл помещу в отдельную выровненную функцию и посмотрю.

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Я привел нормальный код для задержек, используй его и не занимайся ерундой.

А для простой проверки теории - просто nop-ов достаточно в начале msDelay.

alexru ★★★★
()
Последнее исправление: alexru (всего исправлений: 1)
Ответ на: комментарий от alexru

А для простой проверки теории - просто nop-ов достаточно в начале msDelay.

Так будет неправильно, msDelay() постоянно вызывается в основном цикле, и эти лишние нопы будут удлиннять время выполнения msDelay().

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Ты издеваешься?

1. Эта функция сидит в цикле в десятки тысяч инструкции. 5 nop-ов в начале вообще ни на что не повлияют

2. Это предложено для проверки теории, что выравнивание кода значимо.

alexru ★★★★
()
Последнее исправление: alexru (всего исправлений: 1)
Ответ на: комментарий от alexru

Эта функция сидит в цикле в десятки тысяч инструкции. 5 nop-ов в начале вообще ни на что не повлияют

Я не понял, эти nop-ы надо добавить в начале функции msDelay()

void msDelay(int ms)
{
    __asm("nop");
    __asm("nop");
    ...

или в начале чего-то другого?

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Beewek

Я провел следующие эксперименты, и вот что обнаружил.

Перед экспериментом я закомментировал «разгоняющий» вызов otherPortInit(). При его комментировании, как написано в топике, мигание начинает работать медленнее. Моей задачей было «разогнать» мигание каким-то другим способом.

1. Вначале пробовал перед бесконечным циклом добавить nop-ы, чтобы выровнять его:

__asm("nop");
Добавлял от 1 до 16 nop-ов (можно было наверно обойтись и 1...4 так как nop два байта занимает, а выравнивание в теории нужно на 8). Никакого эффекта. Проверил выхлоп дизассемблера, не убрала ли nop-ы оптимизация? Нет, сколько nop-ов написано, столько в коде и есть. То есть, выравнивание начала бесконечного цикла на скорость его работы не влияет. Убрал эти nop-ы.

2. Далее пробовал добавить nop-ы первыми командами внутри основного цикла. Пробовал от 1 до 16, так же никакого эффекта. Убрал эти nop-ы.

3. Далее начал пробовать добавлять nop-ы в самое начало функции msDelay() перед циклом задержки. Когда добавил 5 nop-ов, мигание немного ускорилось, но не сильно (какое-то промежуточное ускорение получилось). А когда добавил 6 nop-ов, мигание стало быстрым. И сколько бы дальше не добавлял nop-ов, мигание было быстрым. Убрал эти nop-ы чтобы продолжить экспермент.

4. Далее попробовал выровнять положение функции msDelay(). Добавил перед функцией:

__attribute__ (( aligned(8) )) - нет эффекта
__attribute__ (( aligned(16) )) - нет эффекта
__attribute__ (( aligned(32) )) - опа, стало мигать быстро. Комментирую эту строку - мигает медленно. Раскомментирую - мигает быстро.

5. Далее вместо выравнивания функции msDelay(), решил ее разместить в ОЗУ. Перед функцией прописал:

__attribute__((noinline, section(".ramfunc")))

- стало мигать быстро. То ли из-за того, что функция в ОЗУ разместилась с выравниванием, то ли из-за того что чтение с ОЗУ более быстрое, не такое как у FLASH. Тут нужен комментарий профессионалов.

Итог. В принципе стало понятно, что скорость работы кода действительно зависит от выравнивания.

Однако осталось неясно, почему ускорения не было в эксперименте №4 при выравнивании на 8 и 16. В данной STM флеш читается более большими блоками, не по 128 бит, а по 512?

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Выравнивание в ОЗУ ни на что не влияет. ОЗУ всегда работает с без задержек.

В остальном - нужно смотреть как код разместился. Влияет не только буфер флеша, но и прочие ускорители. И как там у ST мне разбираться лень, так как особой пользы от этого нет.

alexru ★★★★
()
Ответ на: комментарий от Xintrea

Однако осталось неясно, почему ускорения не было в эксперименте №4 при выравнивании на 8 и 16. В данной STM флеш читается более большими блоками, не по 128 бит, а по 512?

Да кто ж его знает, как оно там работает:) Чем сложнее контроллер, тем меньше гарантий на тайминг выполнения отдельных инструкций и больше сторонних факторов. Всякие ускорители, кэши, и всё такое. Это не PIC/AVR, где время выполнения инструкций известно и неизменно.

Включи таймер, делай задержки на нём.

Beewek ★★★
()
Ответ на: комментарий от alexru


Если что, нормальный код для задержек:

__attribute__((noinline, section(".ramfunc")))
static void delay_ms(int ms)
{
  uint32_t cycles = ms * F_CPU / 3 / 1000;

  asm volatile (
    "1: subs %[cycles], %[cycles], #1 \n"
    "   bne 1b \n"
    : [cycles] "+r"(cycles)
  );
}

Всего один вопрос: а почему этот правильный код при параметре 1000мс дает задержку 4с? Так и задумывалось? У нас настолько разные понятия о времени? Откуда взяласть эта странная формула ms * F_CPU / 3 / 1000? Почему частота делится на три, а не на семь или стопятнадцать?

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

3 - это число тактов в 2-х инструкциях (на одну итерацию цикла). subs - 1 такт и bne - 2 такта. Этот код рабочий для типичных Cortex-M4/M0+. Если этот ST с Cortex-M3 тормознее, то нужно соответственно формулу править.

Формула очевидная - считаем число тактов в 1 мс и умножаем на их колличество.

Хотя странно что он так тормозит. Чего-то не так. У вас точно 72 МГц?

alexru ★★★★
()
Ответ на: комментарий от alexru

Ну и естественно любые прерывания сделают задержку длиннее, так что пробовать нужно с выключенными прерываниями и для секундных задержек в реальных проектах нужно таймеры использовать.

alexru ★★★★
()
Ответ на: комментарий от alexru

Я что-то не пойму, если прерывания специальным образом не настраивать (то есть, вообще кода работы с прерываниями нет), то они все равно есть, какие-то пустые по-дефолту, и их надо обязательно отключать?

Xintrea ★★★★★
() автор топика
Ответ на: комментарий от Xintrea

Нет, если не включены специально, то ничего делать не нужно.

Я бы попробовал с формулой «ms * F_CPU / 1 / 1000». Задержка будет длинной, но пропорциональной итерации цикла в тактах. Потом добавить nop после subs и посмотреть на сколько тактов увеличился цикл.

alexru ★★★★
()
Ответ на: комментарий от Xintrea

Что-то странно. Я бы проверил что он действительно на 72 МГц работает и что функция действительно попала в SRAM.

Ну и для проверки F_CPU определен как 72000000?

alexru ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.