В одной из предыдущих заметок автор пытался рассуждать о том, что при программировании микроконтроллера простой переключатель задач будет полезен в ситуациях, когда использование операционной системы реального времени — это слишком много, а всеобъемлющая петля (super loop) для всех требуемых действий — это слишком мало (Сказал, прямо как граф де Ла Фер). Точнее говоря, не слишком мало, а слишком запутано.


В последующей заметке планировалось упорядочить доступ к общим для нескольких задач ресурсам с помощью очередей на основе кольцевых буферов (FIFO) и специально отведенной для этого отдельной задачи. Разбросав по разным задачам те действия, которые не связаны друг с другом, мы вправе ожидать более обозримый код. А если при этом мы получим некоторое удобство и простоту, то почему бы и не попробовать?


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


В микроконтроллерах, надо сказать, требование считаться со временем как с чем-то важным и жестко заданным встречается чаще, чем в компьютерах общего назначения. Выход за рамки в первом случае приравнивается к неработоспособности, а во втором случае ведет, всего лишь, к увеличению времени ожидания, что вполне допускается, если нервы в порядке. Есть даже два термина «soft real time» и «hard real time».


Напомню, речь шла о контроллерах с ядром Cortex-M3,4,7. На сегодня — очень распространенное семейство. В примерах, представленных ниже, использовался микроконтроллер STM32F303, входящий в состав платы STM32F3DISCOVERY.


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


Первоначально планировалась самая простая логика работы переключателя, которая представлена на рисунке 1 для восьми задач.



В этой схеме задачи поочередно получают свою порцию времени и могут лишь отдать остаток своего тика и, если потребуется, следом пропустить несколько своих тиков. Эта логика показала себя хорошо, поскольку размер кванта можно сделать небольшим. А именно это и требуется для того, чтобы не пытаться срочно поднимать задачу, для которой только что случилось прерывание, а также повышать, а потом понижать ее приоритет. Тот пакет, который только что получен спокойно подождет 200-300 микросекунд, пока его задача не получит свой тик. А если у нас Cortex-M7, работающий на частоте 216 МГц, то 20 микросекунд для одного тика — это вполне разумно, поскольку на переключение уйдет меньше половины микросекунды. И любая задача из примера выше никогда не опоздает больше, чем на 140 микросекунд.


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



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


Для разрешения доступа к разделяемым ресурсам используются две похожих схемы. Первая, которая упоминалась в предыдущей заметке, использует несколько FIFO (или циклических буферов по числу производителей сообщений) и отдельную согласующую задачу. Она предназначена для связи с внешним миром и не требует ожидания от задач, генерирующих сообщения. Надо только следить, чтобы очереди не переполнялись.


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



Основными элементами в ней является буфер запросов, по числу желающих задач, и один индикатор доступа. Работа этой конструкции достаточно проста. Задача слева посылает запрос на доступ в специально отведенное для нее место (например, task 2 записывает 1 в Request 2). Задача – диспетчер выбирает кому разрешить и записывает во флаг разрешения номер выбранной задачи. Задача, получившая разрешение, выполняет свои действия и записывает в запрос признак окончания доступа, значение 0xFF. Планировщик, видя, что запрос снят, обнуляет флаг разрешения, обнуляет прошлый запрос и переходит к запросу от другой задачи.


Два тестовых проекта под IAR и описание используемой платы STM32F3DISCOVERY можно посмотреть здесь. В первом проекте ATS303 просто проверялась работоспособность и проходила отладка. Пригодились все установленные на этой плате светодиоды. Никто не пострадал.


Во втором проекте BTS303 проверялись два упомянутых варианта распределения ресурсов. В нем задачи 1 и 2 генерируют тестовые сообщения, которые поступают оператору. Для связи с оператором пришлось добавить платку с TTL COM портом, как показано на фото ниже.



Со стороны оператора задействован эмулятор терминала. Думаю, читатель извинит автора за мягкий ламповый цвет. Выглядит это так.



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


void  main_start_task_switcher(U8 border);

    U8  task_run_and_return_task_number((U32)t1_task);
    U8  task_run_and_return_task_number((U32)t2_task);
    U8  task_run_and_return_task_number((U32)t3_human_link);
    U8  task_run_and_return_task_number((U32)t4_human_answer);
    U8  task_run_and_return_task_number((U32)task_5);
    U8  task_run_and_return_task_number((U32)task_6);
    U8  task_run_and_return_task_number((U32)task_7);

В этих строчках происходит сначала запуск переключателя, а затем, по очереди, остальных семи задач.


Вот минимальный набор необходимых для работы вызовов.


  void task_wake_up_action(U8 taskNumber);

Этот вызов используется в прерывании от пользовательского аппаратного таймера. Вызовы из самих задач говорят сами за себя.


 void release_me_and_set_sleep_steps(U32 ticks);

    U8 get_my_number(void);

Все эти функции находятся в ассемблерном файле переключателя. Там же есть еще несколько функций, полезных для тестирования, но не обязательных.


В проекте BTS303 задача 3 получает команды оператора извне и отправляет ему ответы на них, которые идут от задачи 4. Задача 4 получает от задачи 3 команды от оператора и выполняет их с возможными ответами. Задача 3 получает также сообщения от задач 1 и 2 и отправляет по UART на эмулятор терминала (например, putty).


Задача 0 (main) производит некоторую вспомогательную работу, например, проверяет количество оставшихся не затронутыми слов в стековой области каждой задачи. Эту информацию может запросить оператор и получить представление об использовании стека. Первоначально для каждой задачи отводится область стека размером 512 байтов (128 слов) и надо следить (хотя бы на этапе отладки), чтобы эти области не приближались к переполнению.


Задачи 5 и 6 делают вычисления над некоторой общей переменной с плавающей точкой. Для этого они запрашивают к ней доступ у задачи 7.


Есть еще одна дополнительная функция, которую можно увидеть в тестовых проектах. Она предназначена для того, чтобы можно было пробуждать задачу не по истечению количества тиков, а по прошествии заданного времени, и выглядит так.


 void wake_me_up_after_milliSeconds(U32 timeMS);

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


Как видим, список всех необходимых вызовов умещается на одной странице.

Комментарии (8)


  1. Cobolorum
    24.07.2019 13:29

    С этого момента "… А именно это и требуется для того, чтобы не пытаться срочно поднимать задачу, для которой только что случилось прерывание,… " можно дальше не читать.


    1. kerosinimus Автор
      26.07.2019 07:44

      Вот это по-нашему, по-большевистски: «Не читал, но осуждаю».


  1. Pelemeshka
    24.07.2019 14:55
    +1

    У STM32 есть прекрасный модуль прерываний NVIC. Этот модуль позволяет создать многозадачность с 16 уровнями приоритета, сами прерывания могут быть вложенными на все 16 уровней, и это прекрасно решает вашу задачу, еще и ресурсов ядра на планировщик не тратит.


    1. kerosinimus Автор
      26.07.2019 07:51

      Я Вам скажу больше. Можно сделать даже 128 уровней прерывания. Для этого есть
      системный регистр AIRCR. Только не совсем понятно, зачем запихивать весь функционал в обработчики прерываний. Через три месяца я такой код сам перестану понимать.


      1. Vadimatorikda
        27.07.2019 06:30

        Я часто практикую следующий метод:

        • в main команда " в сон";
        • вся логика в прерываниях от более чем 50 линий.

        Звучит страшно, да. Однако на деле, при правильном подходе, выходит достаточно компактно и красиво. Главное красиво описать конечный автомат.


        1. kerosinimus Автор
          27.07.2019 17:28

          Конечно, если аккуратно делать и не пытаться нарушить законы физики, то
          все получится. Но, иногда хочется написать так, чтобы еще раз можно было
          использовать, когда время подожмет.
          Ассемблерные куски, например, если предварительно не описать красиво (когда самому нравится) в виде блок-схемы, то отлаживать очень трудно.
          Но, что не сделаешь ради любопытства.


  1. Alex__AW
    25.07.2019 09:21

    Полезные идеи!..
    Может CTM32 не тот процессор на котором потребуются подобные решения, но на МК более низкого уровня подобная реализация «Многозадачности» выглядит хорошим решением. Сам нечто подобное придумывал для ассемблерных программ на PIC микроконтроллерах младшего семейства. Очень помогало создавать решения, требовательные к временным параметрам.


    1. kerosinimus Автор
      25.07.2019 09:31

      Инструкции Cortex-M как раз для этого очень удобные и мощные. Чего стоит только автоматическое сохранение части регистров в стек при прерывании!