Программирование ARM ESP-IDF FreeRTOS SMP Thu, April 25 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

ESP-IDF FreeRTOS SMP Печать
Добавил(а) microsin   

В этой статье (перевод документации [1]) описывается API и отличия в поведении традиционной Vanilla FreeRTOS (подробнее про Vanilla FreeRTOS см. статьи [2, 3].) и её порта ESP-IDF FreeRTOS. Порт ESP-IDF FreeRTOS был реализован для поддержки мультипроцессорной обработки, Symmetric Multiprocessing (SMP) с учетом архитектурных особенностей платформы ESP32. Предполагается, что читатель знаком с основными возможностями Vanilla FreeRTOS (функции, поведение, использование API).

Оригинальная FreeRTOS (далее именуемая Vanilla FreeRTOS) это маленькая и эффективная система реального времени (Real Time Operating System, RTOS), которая в настоящее время поддерживает многие кристаллы MCU и SoC. Однако Многие цели компиляции процессоров серии ESP (такие как ESP32 и ESP32-S3) способны выполнять код на двух ядрах (dual core symmetric multiprocessing, SMP). Таким образом, версия FreeRTOS, которая используется в пакете разработки ESP-IDF [5] (далее эта версия именуется как ESP-IDF FreeRTOS) была модифицирована в контексте SMP. Начальной версией для ESP-IDF FreeRTOS послужила Vanilla FreeRTOS v10.4.3. Модификации в ESP-IDF FreeRTOS позволяют полноценно задействовать в приложениях dual core SMP на кристаллах ESP SoC.

Для дополнительной информации по изменениям, добавленным в ESP-IDF FreeRTOS, см. [6].

[SMP]

SMP (Symmetric Multiprocessing) это вычислительная архитектура, где 2 или большее количество идентичных CPU (ядер) объединены в одну вычислительную систему таким образом, что используют общую основную память, и работают под управлением одной операционной системы. В общем случае систему SMP характеризует следующее:

• Присутствует несколько вычислительных ядер, работающих (почти) независимо друг от друга. У каждого ядра есть свой регистровый файл, свои прерывания и свои обработчики прерываний.
• Для каждого ядра предоставлен идентичный доступ к памяти. Таким образом, фрагмент кода, который обращается к определенному адресу памяти, будет давать одинаковый эффект независимо от ядра, на котором этот фрагмент кода работает.

Основные достоинства системы SMP в сравнении с системой на одном ядре (такая система также называется системой с асимметричной обработкой, Asymmetric Multiprocessing):

• Наличие нескольких CPU позволяет использовать несколько одновременно работающих аппаратных потоков, что повышает общую пропускную способность / производительность системы.
• Наличие симметричной для ядер памяти означает, что потоки могут переключать ядра во время выполнения. В общем случае это может привести к лучшей загрузке CPU.

Хотя SMP-система позволяет потокам переключать ядра, существуют сценарии, в которых поток обязательно должен работать только на определенном ядре. Поэтому в системах SMP также существует функция родственной привязки ядра (core affinity) к потоку, в которой определенно, на каком конкретном ядре поток может работать.

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

SMP на ESP Target. Цели компиляции ESP (targets, такие как ESP32, ESP32-S3) это двуядерные системы на кристалле (dual core SMP SoC). Эти процессоры обладают следующими аппаратными возможностями в контексте утилизации SMP:

• Два идентичных ядра, которые называются CPU0 (например Protocol CPU или PRO_CPU) и CPU1 (например Application CPU или APP_CPU). Это значит, что фрагмент кода будет работать идентично, независимо от того, на каком ядре он работает.

• Симметричная память (с некоторыми небольшими исключениями).

- Если несколько ядер обращаются к одному и тому же адресу памяти, то эти попытки доступа сериализуются на уровне шины памяти.
- Атомарный доступ к одному и тому же адресу памяти достигается через атомарную инструкцию compare-and-swap, предоставляемую ISA [7].

• Прерывания, переключаемые между ядрами (cross-core interrupts), позволяют одному CPU вызвать прерывание, которое будет обрабатываться на другом CPU. Это позволяет ядрам обмениваться сигналами друг с другом.

Важное замечание: псевдонимы PRO_CPU и APP_CPU в системе программирования ESP-IDF обозначают CPU0 и CPU1. Эти имена просто отражают типовые задачи, которые определенные CPU выполняют в приложениях примеров IDF. Обычно задачи, отвечающие за обработку беспроводных сетей (например WiFi или Bluetooth) прикрепляются к CPU0 (поэтому для этого ядра появляется имя PRO_CPU), в то время как остальные задачи приложения обрабатываются другим CPU1 (отсюда появляется имя APP_CPU).

[Задачи (task)]

Создание задачи. Vanilla FreeRTOS предоставляет следующие функции для создания задачи:

xTaskCreate() создает задачу. Память для задачи выделяется динамически.
xTaskCreateStatic() создает задачу. Память для задачи выделяется статически (т. е. предоставляется пользователем).

Однако в системе SMP задачам следует присвоить определенную привязку к ядру (affinity). Таким образом, ESP-IDF предоставляет версию PinnedToCore для функций создания задач, аналогичных функциям Vanilla FreeRTOS:

xTaskCreatePinnedToCore() создает задачу с привязкой к определенному ядру. Память для задачи выделяется динамически.
xTaskCreateStaticPinnedToCore() создает задачу с привязкой к определенному ядру. Память для задачи выделяется статически (т. е. предоставляется пользователем).

API-интерфейс PinnedToCore-версии функций создания задач отличается от соответствующих функций vanilla, поскольку имеют дополнительный параметр xCoreID, который задает привязку к нужному ядру. Для этого параметра допустимы значения:

• 0 означает привязку задачи к CPU0.
• 1 означает привязку задачи к CPU1.
tskNO_AFFINITY позволяет задаче привязаться к обоим ядрам CPU.

Обратите внимание, что ESP-IDF FreeRTOS все еще поддерживает vanilla-версии функций создания задач. Однако они модифицированы таким образом, что просто вызывают свою PinnedToCore-версию с параметром привязки tskNO_AFFINITY.

Важное замечание: ESP-IDF FreeRTOS также меняет единицы параметра размера стека задачи ulStackDepth, который передается в функции создания задачи. Размер стека задачи в Vanilla FreeRTOS указывается в количестве слов, в то время как в ESP-IDF FreeRTOS размер стека указывается в байтах.

Выполнение задачи. Анатомия задачи ESP-IDF FreeRTOS такая же, как и у Vanilla FreeRTOS. Если подробнее, то задачи ESP-IDF FreeRTOS:

• Могут находится только водном из следующих состояний: Running, Ready, Blocked или Suspended.
• Функции задачи обычно реализуются как бесконечный цикл.
• Из функции задачи никогда не производится возврат.

Удаление задачи. В Vanilla FreeRTOS удаление задачи производится вызовом vTaskDelete(). Эта функция позволяет удалить как другую задачу, так и текущую (если в параметре дескриптора задачи передан NULL, то задача удаляет сама себя). Реальное освобождение памяти задачи иногда делегируется задаче ожидания, idle task (если задача удаляет сама себя).

ESP-IDF FreeRTOS предоставляет такую же функцию vTaskDelete(). Однако из-за двуядерной природы кристалла ESP32 существуют отличия в поведении системы при вызове vTaskDelete() в ESP-IDF FreeRTOS:

• Когда удаляется задача, прикрепленная к другому ядру, память удаляемой задачи всегда освобождает idle task другого ядра (из-за необходимости очистки регистров FPU).

• Когда удаляется задача, которая в настоящий момент работает на другом ядре, срабатывает yield (уступка контекста) на другое ядро, и память задачи освобождается одной из idle task (в зависимости от привязки задачи к определенному ядру).

• Память удаляемой задачи освободится немедленно, если:

- Задача в настоящее время работает на этом ядре и также привязана к этому же ядру.
- Задача в настоящее время не работает и не привязана к любому ядру.

Пользователям следует избегать ситуации вызова vTaskDelete() для задачи, которая в настоящий момент работает на другом ядре. Это связано с тем, что трудно определить, что в настоящее время выполняется задача, работающая на другом ядре, таким образом это может привести к следующему непредсказуемому поведению:

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

Там, где это возможно, пользователи должны разработать свое приложение таким образом, чтобы vTaskDelete() вызывалась только для задач в известном состоянии. Например:

• Задачи удаляют саму себя (через vTaskDelete(NULL)), когда их выполнение завершено, и очищены все ресурсы, которые использовались в задаче.
• Задачи помещают сами себя в состояние приостановки (через vTaskSuspend()) перед тем, как они будут удалены другой задачей.

Важное замечание: вызов vTaskDelete(NULL) обязательно стоять в конце тела функции потока. Если это условие не выполнить, то состояние задачи никогда не поменяется на eDeleted, и всегда будет eRunning.

[Планировщик SMP]

Планировщик Vanilla FreeRTOS, работающий как вытесняющий планировщик с фиксированными приоритетами (Fixed Priority Preemptive scheduler) лучше всего описан в [3]. Планировщик распределяет процессорное время квантами времени (Time Slicing), и основной принцип работы следующий:

• Каждая задача при создании получает постоянный приоритет выполнения. Планировщик выполняет задачу с наивысшим приоритетом, которая находится в состоянии готовности к работе (ready state).
• Планировщик может переключить выполнение (контекст) на другую задачу без взаимодействия с текущей выполняющейся задачей.
• Планировщик будет периодически переключать контекст между задачами ready state, у которых одинаковый приоритет (алгоритм переключения по кольцу, round robin). Кванты времени (time slicing) обрабатываются прерыванием тика (по умолчанию длительность тика 1 мс).

Планировщик ESP-IDF FreeRTOS поддерживает те же функции, т. е. фиксированные приоритеты (Fixed Priority), вытесняющая многозадачность (Preemption) и кванты времени (Time Slicing), хотя имеются некоторые незначительные отличия в поведении.

Фиксированные приоритеты. В Vanilla FreeRTOS, когда планировщик выбирает для запуска новую задачу, он всегда выберет текущую ready state задачу самым высоким приоритетом. В ESP-IDF FreeRTOS каждое ядро будет независимо от другого выбирать задачи для запуска. Когда определенное ядро выбрало задачу, это ядро выберет ready state задачу с самым высоким приоритетом, которая может работать на этом ядре. Задача может быть запущена на определенном ядре, если:

• У задачи есть совместимая привязка (compatible affinity), т. е. она либо привязана к этому ядру, либо вообще не привязана к определенному ядру.
• Задача не выполняется другим ядром.

Однако пользователи не должны полагать, что две задачи ready state с самым высоким приоритетом всегда запускаются планировщиком, поскольку необходимо также учитывать core affinity задач. Например, имеются следующие задачи:

• Task A, приоритет 10, привязка к CPU0.
• Task B, приоритет 9, привязка к CPU0.
• Task C, приоритет 8, привязка к CPU1.

В результате планирования Task A заработает на CPU0, и Task C заработает на CPU1. Task B не запустится, потому что у неё второй по уровню приоритет, и его привязка к ядру CPU0 не позволит запуститься на другом ядре.

Вытеснение задачи. В Vanilla FreeRTOS планировщик может вытеснить текущую работающую задачу, если задача с более высоким приоритетом перейдет в состояние готовности к выполнению (ready state). Подобным образом ведет себя ESP-IDF FreeRTOS, но вытеснение происходит индивидуально на каждом ядре, если планировщик определит, что задача с более высоким приоритетом может быть запущена на соответствующем ядре.

Однако в некоторых случаях задача с самым высоким приоритетом, которая переходит в состояние ready, может быть запущена на нескольких ядрах. В этом случае планировщик выполнит вытеснение только на одном ядре. Планировщик всегда дает предпочтение текущему ядру, когда может быть проведено вытеснение на нескольких ядрах. Другими словами, если не привязанная задача с самым высоким приоритетом в состоянии ready имеет приоритет выше, чем задачи, работающие на обоих ядрах, то планировщик всегда выберет вытеснение на текущем ядре. Например, имеются следующие задачи:

• Task A, приоритет 8, сейчас работает на CPU0.
• Task B, приоритет 9, сейчас работает на CPU1.
• Task C, приоритет 10, не привязанная к определенному ядру, была разблокирована задачей Task B.

В результате для запуска Task C будет вытеснена Task B, несмотря на то, что Task A имеет меньший приоритет. Причина в том, что планировщик для вытеснения всегда дает предпочтение текущему ядру.

Интервалы (Time Slicing). Планировщик Vanilla FreeRTOS реализует слайсинг времени, это значит, что если сейчас в состоянии готовности находится несколько задач с самым высоким приоритетом, то каждой из этих задач на выполнение будет по очереди выделяться квант времени (интервал между тиками). Задачи будут вытесняться по очереди циклически, с фиксированным периодом (алгоритм round robin).

Однако в ESP-IDF FreeRTOS невозможно реализовать чистый принцип вытеснения Round Robin, потому что фактически определенная задача не сможет запуститься на определенном ядре по следующим причинам:

• Задача привязана к другому ядру.
• Для не привязанных задач: задача уже выполняется другим ядром.

Таким образом, когда ядро просматривает список ready state, чтобы выбрать задачу для запуска, ядру может потребоваться пропустить несколько задач в этом списке или перейти к более низкому приоритету, чтобы найти ту задачу ready state, которая может запуститься на этом ядре.

Планировщик ESP-IDF FreeRTOS реализует алгоритм слайсинга времени Best Effort Round Robin для задач ready state с одинаковым приоритетом. Этот алгоритм гарантирует, что задачи, которые были выбраны для запуска, перемещаются в конец списка, что дает не выбранным задачам повышенный приоритет на следующей итерации планирования (т. е. на следующем прерывании тика или на yield).

Следующий пример демонстрирует слайсинг времени Best Effort Round Robin в действии. Подразумевается следующее:

• Существует 4 задачи AX, B0, C1, D1 в состоянии ready state с одинаковым приоритетом. Приоритет отдается текущей задаче с самым высоким приоритетом. Первый символ обозначает имена задач (т. е. A, B, C, D), и второй символ обозначают привязку к определенному ядру (X обозначает, что у задачи нет привязки).

• Список задач при поиске задачи для запуска всегда просматривается от начала до конца (от Head до Tail).

--------------------------------------------------------------------------------
1. Начальное состояние. Ни одна из задач ready state не выбрана для запуска.
Head [ AX , B0 , C1 , D0 ] Tail
 
--------------------------------------------------------------------------------
2. Ядро 0 получило прерывание тика и просматривает список задач.
  Выбирается задача A, и она перемещается в конец списка.
Core0--|
Head [ AX , B0 , C1 , D0 ] Tail
                      0
Head [ B0 , C1 , D0 , AX ] Tail
 
--------------------------------------------------------------------------------
3. Ядро 1 получает прерывание тика и просматривает список задач.
  Задача B не может быть запущена из-за несоответствия привязки,
  так что ядро 1 пропускает B и переходит к задаче C.
  Задача C выбирается для запуска и перемещается в конец списка.
Core1-------|         0
Head [ B0 , C1 , D0 , AX ] Tail
                 0    1
Head [ B0 , D0 , AX , C1 ] Tail
 
--------------------------------------------------------------------------------
4. Ядро 0 получает следующее прерывание тика, и просматривает список задач.
  Выбирается задача B, и она перемещается в конец списка.
Core0--|              1
Head [ B0 , D0 , AX , C1 ] Tail
                 1    0
Head [ D0 , AX , C1 , B0 ] Tail
 
--------------------------------------------------------------------------------
5. Ядро 1 получает следующее прерывание тика, и просматривает список задач.
  Задача D не может быть запущена из-за несоответствия привязки, так что
  ядро 1 пропустит задачу D и запустит задачу A. Задача A переместится
  в конец списка.
Core1-------|         0
Head [ D0 , AX , C1 , B0 ] Tail
                 0    1
Head [ D0 , C1 , B0 , AX ] Tail

Последствия для пользователей в контексте работы алгоритма Best Effort Round Robin слайсинга времени:

• Пользователи не могут ожидать, что несколько задач ready state будут запускаться последовательно (как это было в случае Vanilla FreeRTOS). Как было продемонстрировано в примере выше, ядру может понадобиться пропускать задачи.
• Однако с учетом достаточного количества тиков задаче в конечном итоге будет выделено некоторое время для обработки.
• Если ядро не может найти задачу ready state с самым высоким приоритетом, которую можно запустить, то оно при поиске переходит к задачам с более низким приоритетом.
• Для достижения идеальной работы слайсинга round robin time, пользователи должны гарантировать, что все задачи с определенным приоритетом привязаны к одному и тому же ядру.

Прерывания тика. Vanilla FreeRTOS требует возникновения периодических прерываний тика. Прерывание тика отвечает за следующее:

• Инкремент счетчика тиков планировщика.
• Разблокировка любых заблокированных задач, на которых истек таймаут блокировки.
• Проверка, требуется ли слайсинг времени (т. е. выполнение срабатывание переключения контекста).
• Выполнение обработчика тика приложения (application tick hook).

В ESP-IDF FreeRTOS каждое ядро будет получать периодическое прерывание, и на каждом ядре работает независимое прерывание тика. Эти прерывания тика на каждом ядре имеют одинаковый период, однако могут быть не совпадать по фазе. Кроме того, перечисленные выше обязанности, связанные с прерываниями тика, выполняются не всеми ядрами:

• CPU0 выполнит все перечисленные выше обязанности по прерываниям тика.
• CPU1 будет проверять только слайсинг времени и будет выполнять обработчик тика приложения (application tick hook).

Важное замечание: CPU0 полностью отвечает за отслеживание времени в ESP-IDF FreeRTOS. Таким образом все, что мешает CPU0 инкрементировать счетчик тиков (как например приостановка планировщика на CPU0) приведет к отставанию времени всех планировщиков.

Задачи ожидания (Idle Tasks). Vanilla FreeRTOS неявно создаст idle task с приоритетом 0, когда запускается планировщик. Задача idle task работает, когда нет ни одной другой задачи, готовой к запуску, и задача idle отвечает за следующее:

• Освобождение памяти удаленных задач.
• Выполнение обработчика ожидания приложения (application idle hook).

В ESP-IDF FreeRTOS для каждого ядра создается и привязывается отдельная idle task. Задачи idle на каждом ядре делают то же самое, что их аналоги vanilla.

Приостановка планировщика. Vanilla FreeRTOS позволяет приостанавливать планировщик и возобновлять его работу путем вызова vTaskSuspendAll() и xTaskResumeAll() соответственно. Когда планировщик приостановлен:

• Переключение контекста между задачами запрещено, но прерывания остаются разрешенными.
• Не допускается вызов любой функции, которая приводит к блокировке/yield, и слайсинг времени запрещен.
• Счетчик тиков замораживается, однако прерывание тика все еще будет выполнять обработчик тика приложения (application tick hook).

При возобновлении работы планировщика xTaskResumeAll() перехватит все потерянные тики и разблокирует все задачи, у которых истек таймаут.

В ESP-IDF FreeRTOS невозможна приостановка планировщика, распространяющаяся на несколько ядер. Поэтому, когда вызывается vTaskSuspendAll():

• Переключение задач запрещается только на текущем ядре, но прерывания на текущем ядре остаются разрешенными.
• Не допускается на текущем ядре вызов любой функции, которая приводит к блокировке/yield, и слайсинг времени запрещен на текущем ядре.
• Если приостанавливается планировщик на CPU0, то счетчик тиков замораживается. Прерывание тика все еще будет срабатывать для выполнения обработчика тика приложения (application tick hook).

При возобновлении работы планировщика на CPU0 вызов xTaskResumeAll() перехватит все потерянные тики и разблокирует все задачи, у которых истек таймаут.

Предупреждение: если учесть, что приостановка планировщика на ESP-IDF FreeRTOS приостановит планирование задач только на определенном ядре, то приостановка планировщика НЕ ЯВЛЯЕТСЯ допустимым методом, обеспечивающим функционирование взаимного исключения (mutual exclusion) между задачами, когда осуществляется доступ к общим данным/ресурсам. Пользователи должны использовать правильные примитивы блокировки, такие как мьютексы или спинлоки (spinlock), если требуется взаимное исключение.

Запрет прерываний. Vanilla FreeRTOS позволяет запрещать и разрешать прерывания путем вызова taskDISABLE_INTERRUPTS и taskENABLE_INTERRUPTS соответственно.

ESP-IDF FreeRTOS предоставляет такое же API, однако прерывания будут запрещены или разрешены на текущем ядре.

Предупреждение: запрет прерываний будет допустимым методом для достижения mutual exclusion в Vanilla FreeRTOS (и в общем случае на одноядерных системах). Однако в системе SMP, запрет прерываний НЕ ЯВЛЯЕТСЯ допустимым методом, для гарантии работы mutual exclusion. В описании критических секций далее.

Startup и Termination. ESP-IDF FreeRTOS не требует от пользователя вызывать vTaskStartScheduler() для запуска планировщика. Обработка startup в приложении ESP-IDF будет это делать автоматически. Точка входа в приложение это определяемая пользователем функция void app_main (void). Более подробно про startup приложений ESP-IDF FreeRTOS см. раздел "Приложения ESP-IDF FreeRTOS" статьи [8].

ESP-IDF FreeRTOS не поддерживает завершение планировщика. Вызов vTaskEndScheduler() просто приведет к аварийному завершению приложения (abort).

[Критические секции]

Изменения в API. Vanilla FreeRTOS реализует критические секции (см. описание критических секций в статье [4]) путем запрета прерываний. Запрет прерываний устраняет переключения контекста между задачами и обработку ISR во время действия критической секции. Таким образом, когда задача или ISR входит в критическую секцию кода, во время работы в критической секции будет полностью запрещено использование общего ресурса. Критические секции в Vanilla FreeRTOS обслуживаются следующими API-функциями:

taskENTER_CRITICAL() входит в критическую секцию кода путем запрета прерываний.
taskEXIT_CRITICAL() выходит из критической секции путем разрешения прерываний.
taskENTER_CRITICAL_FROM_ISR() из ISR входит в критическую секцию кода путем запрета вложенности прерываний.
taskEXIT_CRITICAL_FROM_ISR() выходит из критической секции ISR путем разрешения вложенности прерываний.

Однако в системе SMP простой запрет прерываний не создает критическую секцию, поскольку наличие других ядер означает, что общий ресурс остается конкурентно доступен. Поэтому критические секции в ESP-IDF FreeRTOS реализованы с использованием spinlock-ов. Для размещения spinlock-ов API-функции критической секции ESP-IDF FreeRTOS содержат дополнительный параметр spinlock, как показано ниже:

• Spinlock-и представлены типом portMUX_TYPE (не перепутайте с мьютексами FreeRTOS).
taskENTER_CRITICAL(&mux) производит вход в критическую секцию из контекста задачи.
taskEXIT_CRITICAL(&mux) выходит из критической секции в контексте задачи.
taskENTER_CRITICAL_ISR(&mux) входит в критическую секцию из контекста прерывания.
taskEXIT_CRITICAL_ISR(&mux) выходит из критической секции в контексте прерывания.

Важное замечание: API-функции критической секции можно вызывать рекурсивно (т. е. допускаются вложенные друг в друга критические секции). Вход в критическую секцию несколько раз допустим, пока выход из критической секции производится столько же раз, сколько раз был выполнен вход. Однако с учетом того факта, что критические секции могут быть на целены на разные spinlock-и, пользователи должны быть внимательными к тому, что потенциально может возникнуть глухая блокировка (dead lock), когда происходит рекурсивный вход в критические секции.

Реализация. В ESP-IDF FreeRTOS процесс обработки входа в критическую секцию и выхода из неё происходит следующим образом:

• Для taskENTER_CRITICAL(&mux) (или taskENTER_CRITICAL_ISR(&mux))

1. Ядро запрещает свои прерывания (или вложенность прерываний) до configMAX_SYSCALL_INTERRUPT_PRIORITY.
2. Затем ядро крутится (spin) на spinlock, используя инструкцию compare-and-set до тех пор, пока не получит блокировку (lock). Блокировка получена, когда ядро может установить значение владельца lock на идентификатор ядра (core ID).
3. Как только spinlock захвачен, происходит возврат из функции taskENTER_CRITICAL (или taskENTER_CRITICAL_ISR). Остальная часть критической секции работает в ситуации запрещенных прерываний (или ситуации запрещения вложенности прерываний).

• Для taskEXIT_CRITICAL(&mux) (или taskEXIT_CRITICAL_ISR(&mux))

1. Ядро освобождает spinlock путем очистки значения владельца spinlock.
2. Ядро разрешает прерывания (или вложенность прерываний).

Ограничения и соглашения. С учетом того, что прерывания (или вложенность прерываний) в критической секции запрещены, существует несколько ограничений касательно того, что можно делать в пределах критической секции. Во время критической секции пользователи должны учитывать следующее:

• Код критической секции должен быть максимально коротким по времени выполнения.

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

• В пределах критической секции не должны вызываться API-функции FreeRTOS.

• В критической секции не должны вызваться любые функции, вызывающие блокировку (blocking) или уступку контекста (yielding).

[Использование арифметики с плавающей точкой]

Обычно при переключении контекста происходит следующее:

• Текущее состояние регистров CPU сохраняется в стеке той задачи, которая вытесняется.
• Ранее сохраненное состояние регистров CPU загружается из стека той задачи, работа которой возобновляется.

Однако ESP-IDF FreeRTOS реализует технику Lazy Context Switching для регистров FPU (Floating Point Unit) ядра. Другими словами, когда происходит переключение контекста на определенном ядре (например CPU0), состояние регистров FPU ядра не сохраняется немедленно в стек при вытеснении задачи (например Task A). Регистры FPU остаются нетронутыми до момента, пока:

• Другая задача (например Task B) запускается на том же ядре и использует FPU. Это приведет к исключению, которое сохранит регистры FPU в стек задачи Task A.
• Задача Task A получает контекст на том же ядре и продолжает свое выполнение. Для этого случае сохранение и восстановление регистров FPU не требуется.

Однако с учетом того, что задачи могут быть откреплены от определенного ядра, т. е. их запуск может быть запланирован на разных ядрах (например, Task A активируется на CPU1), невозможно копировать и восстанавливать регистры FPU между ядрами. Поэтому, когда задача использует FPU (путем использования типа float в своем коде), ESP-IDF FreeRTOS автоматически прикрепит такую задачу к текущему ядру, на котором задача в настоящий момент работает. Это гарантирует, что все задачи, которые используют FPU, будут привязаны к определенному ядру.

Кроме того, ESP-IDF FreeRTOS по умолчанию не поддерживает использование FPU в контексте прерывания с учетом того, что состояние регистров FPU связано с конкретной задачей.

Важное замечание: цели компиляции ESP (targets), которые содержат FPU, не поддерживают аппаратное ускорение для арифметики плавающей запятой двойной точности (не поддерживают double). Вместо этого double реализовано программно, поэтому ограничения по поведению, накладываемые на тип float, не применяются к типу double. Обратите внимание, что из-за отсутствия аппаратной поддержки double операции могут потреблять значительно больше процессорного времени, чем операции float.

[ESP-IDF FreeRTOS Single Core]

Хотя ESP-IDF FreeRTOS использует планировщик SMP, некоторые цели компиляции ESP содержат в себе одно ядро (такие как ESP32-S2 и ESP32-C3). Когда приложения ESP-IDF компилируются для таких целей, ESP-IDF FreeRTOS все еще используется, однако количество ядер устанавливается в 1 (т. е. CONFIG_FREERTOS_UNICORE будет всегда разрешено для одноядерных целей компиляции).

Для многоядерных целей (таких как ESP32 и ESP32-S3), также можно установить CONFIG_FREERTOS_UNICORE. Это приведет к тому, что ESP-IDF FreeRTOS будет работать только на CPU0, и все другие ядра будут неактивными.

Важное замечание: пользователи должны иметь в виду, что разрешение опции CONFIG_FREERTOS_UNICORE не эквивалентно запуску Vanilla FreeRTOS. Дополнительные API-функции ESP-IDF FreeRTOS все еще могут быть вызваны, и изменение в поведении ESP-IDF FreeRTOS приведет к небольшим дополнительным расходам, даже когда ESP-IDF FreeRTOS скомпилирована только для одного ядра.

[Словарик]

CPU Central Processor Unit, процессор.

FPU Floating-Pount Unit, блок поддержки операций с плавающей точкой.

ISA Instruction Set Architecture.

ISR Interrupt Service Routine, обработчик прерывания.

MCU MicroController Unit, микроконтроллер.

SoC System On Chip, система на кристалле. Чип, интегрирующий в одной микросхеме CPU, периферию и множество специализированных функций.

[Ссылки]

1. ESP-IDF FreeRTOS (SMP) site:docs.espressif.com.
2. FreeRTOS™ Real-time operating system for microcontrollers site:freertos.org.
3. FreeRTOS: практическое применение, часть 1 (управление задачами).
4. FreeRTOS: практическое применение, часть 4 (управление ресурсами).
5. Установка среды разработки ESP-IDF для ESP32.
6. ESP32: функции поддержки FreeRTOS.
7. Xtensa® Instruction Set Architecture site:loboris.eu.
8. ESP-IDF FreeRTOS Task API.
9ESP32 Inter-Processor Call.

 

Добавить комментарий


Защитный код
Обновить

Top of Page