FreeRTOS: практическое применение, часть 3 (управление прерываниями) |
![]() |
Добавил(а) microsin | ||||||||||||||||||||||||||||||||||||||||||||
[3.1. Введение: о чем говорится в части 3] События Встраиваемые системы реального времени должны предпринимать некоторые действия в ответ на события (event), приходящие от внешнего окружения. Например пакет, который пришел в периферийное устройство Ethernet (событие) может требовать своей передачи в стек TCP/IP для обработки (действие). [Предыдущая часть FreeRTOS: практическое применение, часть 2 (управление очередями)] Сложные системы будут иметь службу событий, которые приходят от разных источников, которые требуют разные процедуры обработки и разные требования к времени реакции. В каждом случае должно быть принято решение для реализации лучшей стратегии обработки события: 1. Как событие должно детектироваться? Обычно используются прерывания, но может также быть применен опрос входов (polling). 2. Когда используются прерывания, как много действий по обработке должно быть предпринято внутри обработчика прерывания (ISR), и как много действий нужно сделать вне обработчика прерывания? Обычно желательно сохранять размер ISR как можно меньше по времени выполнения. 3. Как события должны обмениваться информацией с главным кодом (не ISR), и как этот код должен быть структурирован, чтобы лучше всего совместить обработку потенциально асинхронных процессов? FreeRTOS не предоставляет разработчику приложения никакую специальную стратегию для обработки событий, но дает возможности для реализации выбранной стратегии простым и хорошо поддерживаемым методом. Всегда помните, что любые функции API и макросы, которые оканчиваются на ‘FromISR’ или ‘FROM_ISR’, должны быть всегда использованы внутри обработчика прерывания (interrupt service routine, ISR). Эта часть дает читателю хорошее понимание следующего: · Какие функции FreeRTOS API можно использовать из ISR. [3.2. Обработка отложенных (deferred) прерываний] Двоичные семафоры, используемые для синхронизации Двоичные семафоры могут использоваться для разблокировки задачи каждый раз, когда возникает какое-то отдельное прерывание, чем можно эффективно засинхронизировать задачу с прерыванием. Это позволяет разместить почти весь код обработки события прерывания внутри синхронизированной задачи, оставляя в ISR только очень малую часть кода. Такая обработка прерывания называется 'отложенной' (deferred) для задачи-обработчика события. Если обработка прерывания критична по времени реакции, то приоритет задачи обработки может быть назначен соответственно выше, чтобы обеспечить вытеснение обработчиком других задач в системе. Это произойдет сразу после завершения ISR. В результате достигается эффект обработки всех событий в основном коде, как если бы эта обработка была реализована непосредственно в коде ISR. Такая схема демонстрируется на рисунке 26. Рис. 26. Прерывание останавливает одну задачу, при этом делая возврат в другую задачу Задача обработчика использует блокирующий на семафоре вызов 'take' (взять), что подразумевает вхождение в состояние Blocked для ожидания момента события. Когда событие произойдет, ISR использует операцию 'give' (давать) на том же самом семафоре для разблокировки задачи обработчика, чтобы могла произойти требуемая обработка события. Концепция семафора 'take' (взять) и 'give' (давать) может подразумевать разное, в зависимости от сценария использования. В классической терминологии семафоров 'взять семафор' эквивалентно операции P(), а 'дать семафор' эквивалентно операции V(). В этом сценарии синхронизации прерывания семафор концептуально может считаться очередью с длиной, равной 1. Очередь может содержать максимум один элемент в любой момент времени, так что очередь может быть либо пуста, либо полна (как двоичный флаг может быть либо сброшен, либо установлен, поэтому семафор двоичный). Путем вызова xSemaphoreTake() задача обработчика пытается прочитать из очереди на все время блокировки, что приведет ко входу задачи обработчика в состояние Blocked, если очередь пуста (т. е. событие пока не произошло). Когда событие произойдет, то ISR просто будет использовать функцию xSemaphoreGiveFromISR() для помещения токена (семафора) в очередь, что сделает очередь заполненной. Это приведет к выходу задачи обработчика из состояния Blocked, которая удалит токен, снова очистив очередь. Как только задача обработчика завершит свою обработку, она снова сделает попытку чтения из очереди, найдет очередь пустой и снова войдет в состояние Blocked для ожидания следующего события. Эта последовательность показана на рисунке 27. На рисунке 27 видно, что прерывание 'дает' семафор в любом случае, даже если он еще не 'взят', и задача 'берет' семафор, но никогда не отдает его обратно. Поэтому описанный сценарий концептуально эквивалентен записи и чтению через очередь. Это часто вызывает путаницу, поскольку в других семафорных сценариях используются другие правила, где задача, которая берет семафор, должна всегда его отдать - такой сценарий описан в части 4. API функция vSemaphoreCreateBinary() Хендлы ко всем различным типам семафоров FreeRTOS сохраняются в переменной типа xSemaphoreHandle. Перед тем, как семафор можно реально использовать, он должен быть сначала создан. Для создания двоичного семафора используется API функция vSemaphoreCreateBinary(). Примечание: API семафоров реально реализовано как набор макросов, это не функции. Для упрощения изложения в этой книге эти макросы называются функциями.
Листинг 41. Прототип API функции vSemaphoreCreateBinary() Таблица 12. Параметры функции vSemaphoreCreateBinary()
Рис. 27. Использование двоичного семафора для синхронизации задачи и прерывания API функция xSemaphoreTake() 'Взять' (Take) семафор означает 'получить' или 'принять' семафор. Семафор можно взять только если он доступен. В классической терминологии семафоров xSemaphoreTake() эквивалентна операции P(). Все различные типы семафоров FreeRTOS, за исключением рекурсивных семафоров, могут быть 'взяты' с использованием функции xSemaphoreTake(). xSemaphoreTake() нельзя использовать из ISR.
Листинг 42. Прототип API функции xSemaphoreTake() Таблица 13. Параметры и значение возврата функции xSemaphoreTake()
API функция xSemaphoreGiveFromISR() Все различные типы семафоров FreeRTOS, за исключением рекурсивных семафоров, могут быть 'выданы' с использованием функции xSemaphoreGiveFromISR(). xSemaphoreGiveFromISR() - специальная форма функции xSemaphoreGive(), которая специально предназначена для использования из ISR.
Листинг 43. Прототип API функции xSemaphoreGiveFromISR() Таблица 14. Параметры и значение возврата функции xSemaphoreGiveFromISR()
Пример 12. Использование двоичного семафора для синхронизации задачи с прерыванием Этот пример использует двоичный семафор для разблокировки задачи из обработчика прерывания (ISR) - чем достигается эффективная синхронизация задачи с прерыванием. Простая периодическая задача используется для генерации программного прерывания (software interrupt) каждые 500 миллисекунд. Программное прерывание используется для удобства, потому что трудно отслеживать реальные прерывания IRQ от эмулированного DOS-окружения. Листинг 44 показывает реализацию периодической задачи. Имейте в виду, что задача печатает строку перед и после генерации прерывания. Это позволяет явно продемонстрировать в выводе последовательность выполнения приложения.
Листинг 44. Реализация задачи, которая периодически генерирует программное прерывание в примере 12 Листинг 45 показывает реализацию задачи обработчика - задача, которая синхронизирована с программным прерыванием через использование двоичного семафора. Снова сообщение выводится на печать при каждой итерации задачи, так что последовательность, в котором выполняются задача и прерывание, хорошо видна при выполнении примера.
Листинг 45. Реализация задачи обработчика (handler task), т. е. задачи, которая засинхронизирована с прерыванием, в примере 12 Листинг 46 показывает реальный обработчик прерывания ISR. Он выполняет очень мало действий, кроме как 'выдает' семафор для разблокировки задачи обработчика. Обратите внимание, как используется параметр pxHigherPriorityTaskWoken. Он устанавливается в значение pdFALSE перед вызовом xSemaphoreGiveFromISR(), с выполнением переключения контекста, если впоследствии было обнаружено, что pxHigherPriorityTaskWoken равно pdTRUE. Синтаксис определения ISR и вызов макроса для переключения контекста привязан к специфической платформе порта Open Watcom DOS, и может отличаться от других портов. Пожалуйста обратитесь к примерам, которые включены в демо-приложения для используемого Вами порта FreeRTOS, чтобы найти актуальный, требуемый для Вашего порта синтаксис.
Листинг 46. Обработчик программного прерывания, используемый в примере 12 Функция main() просто создает двоичный семафор и задачи, инсталлирует обработчик прерывания и запускает шедулер. Реализация показана в листинге 47.
Листинг 47. Реализация функции main() примера 12 Пример 12 производит вывод, показанный на рисунке 28. Как и ожидалось, задача обработчика (Handler) запускается немедленно после генерации прерывания (Interrupt), так что вывод из задачи обработчика делит на части вывод, производимый периодической задачей (Periodic). Более подробно процесс описан на рисунке 29. Рис. 28. Вывод, который производит при выполнении пример 12 Рис. 29. Последовательность выполнения примера 12 [3.3. Семафоры со счетчиком] Пример 12 продемонстрировал двоичный семафор, который использовался для синхронизации задачи с прерыванием. Последовательность выполнения была следующей: 1. Происходит прерывание. Этот сценарий хорошо подходит для случая, когда прерывания могут произойти только с относительно низкой частотой. Если же произойдет другое прерывание до того, как задача обработчика завершит свою обработку первого захваченного семафором события, то двоичный семафор эффективно фиксировал бы событие, позволяя задаче обработчика немедленно обработать новое событие после обработки предыдущего. Задача обработчика не вошла бы в состояние Blocked между обработкой этих двух событий, так как фиксируемый семафор был бы доступен сразу, как только произойдет вызов xSemaphoreTake(). Этот случай показан на рисунке 30. Рис. 30. Двоичный семафор может фиксировать не больше одного события Рисунок 30 демонстрирует, что двоичный семафор может захватить самое бОльшее только одно событие прерывания. Все последующие события, произошедшие перед обработкой уже захваченного события, будут потеряны. Этого можно избежать, если использовать семафор со счетчиком вместо двоичного семафора. Так же, как двоичные семафоры можно концептуально сопоставить с очередями, длина которых равна 1, семафоры со счетчиком можно представить как очереди, длина которых больше 1. Задачам не интересны данные, которые сохранены в такой очереди - им просто нужно знать, пуста очередь или нет. Каждый раз, когда семафору со счетчиком 'дают', в его очереди задействуется пространство (очередь становится непустой). Количество элементов (событий) в очереди показывает значение счетчика семафора. Рис. 31. Использование семафора-счетчика для 'подсчета' событий. Семафоры со счетчиком обычно используют в следующих случаях: 1. Подсчет событий. В этом сценарии использования ISR 'выдает' семафор каждый раз, когда происходит событие - что инкрементирует значение счетчика семафора при каждой выдаче семафора. Задача обработчика события будет 'брать' семафор каждый раз, когда она обрабатывает событие - что будет декрементировать счетчик семафора при каждом получении семафора. Значение счетчика семафора отражает разницу между количеством произошедших событий и количеством уже обработанных событий. Этот механизм показан на рисунке 31. Семафоры со счетчиком, используемые для подсчета событий, создаются с начальным значением счетчика, равным 0. 2. Управление ресурсами. В этом сценарии использования значение счетчика показывает количество доступных ресурсов. Для получения контроля над ресурсом задача должна сначала получить семафор - и этим уменьшить значение счетчика семафора. Когда значение счетчика семафора станет равным 0, то это будет означать, что больше нет свободных ресурсов. Когда задача завершает работу с ресурсом, она 'выдает' семафор обратно - увеличивая значение счетчика семафора. Семафоры со счетчиком, которые используются для управления ресурсами, создаются с начальным значением счетчика, равным количеству доступных ресурсов. В части 4 рассматривается использование семафоров для управления ресурсами. API функция xSemaphoreCreateCounting() Хендлы ко всем различным типам семафоров FreeRTOS сохраняются в переменной типа xSemaphoreHandle. Перед тем, как семафор можно реально использовать, он должен быть сначала создан. Для создания семафора со счетчиком используется API функция xSemaphoreCreateCounting().
Листинг 48. Прототип API функции xSemaphoreCreateCounting() Таблица 15. Параметры и значение возврата функции xSemaphoreCreateCounting()
Пример 13. Использование считающего семафора для синхронизации задачи с прерыванием Пример 13 улучшает реализацию примера 12 путем использования семафора со счетчиком вместо двоичного семафора. Функция main() изменена для использования вызова xSemaphoreCreateCounting() вместо vSemaphoreCreateBinary(). Вызов новой API функции показан в листинге 49.
Листинг 49. Использование xSemaphoreCreateCounting() для создания семафора со счетчиком Для симуляции возникновения нескольких событий с высокой частотой изменена подпрограмма обработчика прерывания ISR, чтобы 'выдавать' семафор больше одного раза за одно прерывание. Каждое событие захватывается в значении счетчика семафора. Измененная ISR показана в листинге 50.
Листинг 50. Реализация обработчика прерывания ISR, используемого в примере 13 Все остальные функции остались неизменными, как в примере 12. Вывод, производимый примером 13, показан на рисунке 32. Как можно увидеть, задача обработчика обрабатывает все три (симулированные) события каждый раз, когда генерируется прерывание. События захватываются с значении счетчика семафора, что позволяет задаче обработчика обработать все события друг за другом. Рис. 32. Вывод, который производит при выполнении пример 13 [3.4. Использование очередей внутри обработчика прерывания (ISR)] Функции xQueueSendToFrontFromISR(), xQueueSendToBackFromISR() и xQueueReceiveFromISR() являются защищенными версиями функций xQueueSendToFront(), xQueueSendToBack() и xQueueReceive() соответственно, которые можно безопасно использовать в коде ISR. Семафоры используются для передачи сообщений (из ISR в основную программу). Очереди используются как для передачи сообщений, так и для передачи данных (из ISR в основную программу). API функции xQueueSendToFrontFromISR() и xQueueSendToBackFromISR() xQueueSendFromISR() является эквивалентной и полностью соответствует xQueueSendToBackFromISR().
Листинг 51. Прототип API функции xQueueSendToFrontFromISR()
Листинг 52. Прототип API функции xQueueSendToBackFromISR() Таблица 16. Параметры и значение возврата функций xQueueSendToFrontFromISR() и xQueueSendToBackFromISR()
Эффективное использование очереди Большинство демо-приложений, включенных в загружаемый пакет FreeRTOS, включают в себя простой драйвер UART, который использует очереди для передачи символов в обработчик прерывания передачи и для передачи символов из обработчика прерывания приема. Каждый символ, который был отправлен или принят, индивидуально передается через очередь. Драйверы UART реализованы таким способом только для того, чтобы продемонстрировать использование очереди изнутри ISR. Индивидуальная передача символов через очередь чрезвычайно неэффективна (особенно для высоких скоростей), и не рекомендуется для рабочего кода готовых изделий. Более эффективная техника включает в себя следующее: · Размещение каждого принятого символа в простейший буфер RAM, который использует семафор для разблокировки задачи, обрабатывающей буфер после полного приема сообщения, или после детектирования в потоке признака break. Пример 14. Отправка и прием на очереди с использованием прерывания Этот пример демонстрирует использование xQueueSendToBackFromISR() и xQueueReceiveFromISR() внутри одного и того же прерывания. Как и было ранее, для удобства в примере используется программное прерывание. В примере создается периодическая задача, которая отправляет пять чисел в очередь каждые 200 миллисекунд. Она генерирует программное прерывание только после того, как все пять значений будут отправлены. Реализация задачи показана в листинге 53.
Листинг 53. Реализация задачи примера 14, которая записывает в очередь ISR делает повторы вызовов xQueueReceiveFromISR() до тех пор, пока не будут удалены из очереди все значения, записанные в неё, т. е. пока очередь не станет пустой. Последние два бита каждого принятого значения используются как индекс в массиве строк, и указатель на строку в соответствующей индексной позиции будет отправлен в другую очередь с использованием вызова xQueueSendFromISR(). Реализация ISR показана в листинге 54.
Листинг 54. Реализация обработчика прерывания ISR, используемого в примере 14 Задача, которая принимает указатели на строки из ISR, просто блокируется на очереди, до того момента, как в очереди появится сообщение (указатель на строку), и печатает каждую строку, как она была принята. Реализация принимающей строки задачи показана в листинге 55.
Листинг 55. Задача в примере 14, которая печатает строки, принятые из обработчика прерывания ISR Как обычно, функция main() создает требуемые очереди и задачи перед запуском шедулера. Её реализация показана в листинге 56.
Листинг 56. Функция main() для примера 14 Вывод, производимый примером 14, показан на рисунке 33. Как можно увидеть, ISR принимает 5 целых чисел и в ответ производит 5 строк. Более подробно процесс рассматривается на рисунке 34. Рис. 33. Вывод, который производит при выполнении пример 14. Рис. 34. Последовательность выполнения задач из примера 14. [3.5. Вложенные прерывания] Свежие порты FreeRTOS позволяет вложенность прерываний, т. е. во время работы одного ISR может сработать другое прерывание, и будет запущен другой ISR (это называется вложенность прерываний друг в друга). Эти порты требуют одной или двух констант, заданных в хедере FreeRTOSConfig.h. Константы описаны в таблице 17. Примечание переводчика: к сожалению, в настоящее время (октябрь 2011 года, FreeRTOSv7.0.2) поддержка констант configKERNEL_INTERRUPT_PRIORITY и configMAX_SYSCALL_INTERRUPT_PRIORITY есть только на небольшом количестве платформ (например на Atmel ARM7 этого пока нет, хотя архитектура позволяет назначать приоритеты аппаратных прерываний). Среди портов, которые содержат установку configKERNEL_INTERRUPT_PRIORITY, имеются пока только Cortex-M3, PIC24, dsPIC, PIC32, SuperH и RX600. Среди портов, которые содержат установку configMAX_SYSCALL_INTERRUPT_PRIORITY, имеются пока только PIC32, RX600 и Cortex M3. Таблица 17. Константы, управляющие вложенностью прерываний
Модель полной поддержки вложенности прерываний создается путем установки configMAX_SYSCALL_INTERRUPT_PRIORITY в приоритет выше, чем configKERNEL_INTERRUPT_PRIORITY. Это демонстрируется на рисунке 35, который показывает гипотетический сценарий, где configMAX_SYSCALL_INTERRUPT_PRIORITY установлено в значение 3, и configKERNEL_INTERRUPT_PRIORITY установлено в 1. Как показано, эквивалентный гипотетический микроконтроллер имеет семь разных уровней приоритета. Значение 7 - просто произвольное число для этого гипотетического примера, и оно вовсе не означает, что будет представлено в какой-то конкретной архитектуре микроконтроллера. Обычно возникает путаница между приоритетами задачи и приоритетами прерывания. Рисунок 35 показывает приоритеты прерывания, как они представлены в архитектуре микроконтроллера. Это приоритеты, поддерживаемые на уровне аппаратуры, и ISR разных прерываний выполняются с разными приоритетами друг относительно друга. Задачи не запускают ISR, так что приоритет, назначенный задаче, никак не относится к приоритету, назначенному на источник прерывания. Рис. 35. Константы, влияющие на поведение вложенности прерываний Комментарии к рисунку 35: · Прерывания, которые используют приоритеты от 1 до 3 включительно, не смогут запуститься (будут отложены) на время выполнения ядра FreeRTOS или на время выполнения кода в пределах критических секций приложения. Однако эти прерывания могут свободно использовать (защищенные для прерываний версии) вызовы функций API FreeRTOS. · На прерывания, которые используют уровень 4 и выше, никак не влияют критические секции, и никакие действия ядра FreeRTOS не могут повлиять на немедленный запуск этих прерываний - за исключением ограничений только самого микроконтроллера (как если бы FreeRTOS вообще не существовало). Обычно функциональность, необходимая для очень высокой точности времени выполнения (например, для управления двигателем), использует приоритет для ISR выше configMAX_SYSCALL_INTERRUPT_PRIORITY, чтобы гарантировать, что выполнение шедулера не приведет к дрожанию времени отклика прерываний. · Прерывания, которые не делают никаких вызовов функций FreeRTOS API, могут свободно использовать любой приоритет прерываний. Примечание для пользователей ARM Cortex M3 Микроконтроллеры Cortex M3 используют обратную нумерацию приоритетов прерывания - приоритет с низким числовым номером логически соответствует высокому приоритету прерывания. Это кажется парадоксальным, и об этом легко забыть! Если хотите назначить прерыванию низкий приоритет, то Вы должны назначить ему бОльшее числовое значение приоритета. Не назначайте ему приоритет 0 (или другое число с малым значением), так как в результате прерывание получит самый высокий приоритет в системе 0 - и таким образом потенциально может привести систему к полному отказу, если приоритет прерывания окажется выше configMAX_SYSCALL_INTERRUPT_PRIORITY. Самый низкий приоритет в ядре Cortex M3 равен 255, хотя разные поставщики ядер Cortex M3 реализуют разное количество бит приоритета и предоставляют функции библиотек, которые рассчитаны на указание приоритетов разными способами. Например, для STM32 самый низкий приоритет может быть указан в библиотеке драйвера ST как 15, а самый высокий приоритет может быть указан как 0. [Следующая часть FreeRTOS: практическое применение, часть 4 (управление ресурсами)] [Ссылки] 1. Обзор FreeRTOS на русском языке - Андрей Курниц, статья в журнале «Компоненты и технологии» (2..10 номера 2011 года). |