Available Languages?:

OSA : Учебник. Введение

Вступление

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

Во "Введении" будут рассмотрены общие вопросы применения OSA. Здесь будет рассказано о том, как реализуется многозадачность на одном контроллере, показана структура взаимодействия программы и ядра ОС, а также будут приведены особенности программ, использующих OSA.

Предполагается, что читатель уже знаком с PIC-контроллерами и языком программирования Си.

Определения

Для начала дадим несколько определений.

Ядро

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

Задачи

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

Приоритет

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

Планировщик

- подпрограмма в составе ядра, занимающаяся поиском готовых к выполнению задач, выделением самой приоритетной из них и передачей ей управления.

Сервисы ОС

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

Ресурсы

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

Ресурсами являются:

  • в первую очередь - модуль выборки и выполнения команд, программный счетчик, АЛУ, аккумулятор (WREG) и регистр состояния (STATUS) (в один момент времени выполняться может только одна задача);
  • оперативная память (каждая задача может по-своему использовать память);
  • внутренняя EEPROM (пока одна задача выполняет запись, другие не могут производить ни запись ни чтение);
  • встроенные периферийные модули: последовательный порт, параллельный порт, АЦП, таймеры, порты ввода/вывода и т.д. (несколько задач могут выводить данные через USART; один и тот же вывод контроллера используется для разных целей);
  • какие-то внешние устройства, подключенные к контроллеру: LCD, EEPROM, DAC и т.д.

Многозадачность

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

Не вдаваясь в подробности, рассмотрим, как выглядит многозадачность на практике. Рассмотрим для простоты параллельное выполнение двух процессов (задач): Task1 (голубой график) и Task2 (зеленый график).

Рис.1

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

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

Контроллер должен знать, когда и какую последовательность команд выполнять, когда и как переключаться между ними. Для этого и служит операционная система. И на уровне операционной системы многозадачность будет выглядеть так, как показано на рис.1в. Обратим внимание, что здесь добавлен еще один график - желтый - ядро операционной системы. Прерываясь, задача Task1 передает управление не задаче Task2, а планировщику операционной системы (в кооперативных ОСРВ именно задача передает управление планировщику, в отличие от вытесняющих, где сам планировщик либо более приоритетная задача отнимают управление у менее приоритетной), который в свою очередь принимает решение о том, какой задаче отдать управление. В нашем случае он отдает управление задаче Task2. Когда Task2 отдает управление планировщику, у Task1 появляется шанс получить управление вновь.

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

Здесь, наверное, может возникнуть вопрос: для чего нужны сложности с планировщиком, если можно в вечном цикле (часто называют суперциклом или суперлупом) по очереди вызывать функции, содержащие код для Task1 и Task2? Штука в том, что при вызове функции мы каждый раз попадаем в ее начало, в то время как ОС позволяет нам прервать функцию в любом месте, позволив выполниться другим функциям, а затем продолжить ее выполнение с того места, где она была приостановлена. (Это основное отличие от суперцикла. На остальных отличиях пока не будем заострять внимание.) Благодаря этой особенности, каждая задача может быть написана как отдельная независимая программа (она должна быть написана с учетом некоторых требований, но об этом чуть ниже).

Структура программы под OSA

На рис.2 схематически изображено взаимодействие программы пользователя с ядром операционной системы. Все, что находится внутри желтого прямоугольника, - это ядро ОС, скрытое от программиста. Взаимодействие с ним производится только через сервисы ОС.

Рис.2

Логически программа получается разбита на 5 частей:

  • ядро системы (желтое);
  • main() - основная функция Си-программы;
  • прерывание (красное) - код прерывания; обратим внимание, что рядом с прерыванием стоит OS_Timer(), присоединенный к нему пунктирной линией; такая условная привязка показывает, что сервис OS_Timer чаще всего (но не всегда) вызывается именно из прерывания;
  • задачи (светло-голубые) - Си-функции, которые предполагается использовать как параллельно выполняющиеся задачи ОС;
  • функции - все вспомогательные функции.

Сердцем ядра является планировщик, который оперирует с массивом задач, выбирая из него самую приоритетную из готовых и передавая ей управление. Массив задач - это массив дескрипторов, содержащих для каждой активной задачи данные, необходимые для работы с ней: текущий адрес или точка возврата в задачу, состояние задачи (готова, ждет событие, пауза, задержка), таймер задачи (у каждой - свой) и некоторые дополнительные данные в зависимости от используемой платформы (например, для MCC18 - значение регистра FSR1, т.е. указателя стека; для MCC30 - регистры общего назначения W8..W15).

Передача управления внутри системы

Красными линиями показаны пути передачи управления внутри программы. Причем светло-красные обозначают вызов подпрограммы (т.е. подразумевает возврат из нее), а темно-красные - передачу управления безусловным переходом (без возврата). Обратим внимание на три момента:

  1. передача управления от OS_Run() планировщику ОС производится только в одном направлении. Это означает, что после вызова сервиса OS_Run управление берет на себя ядро ОС, т.е. на этом вызове выполнение функции main() заканчивается;
  2. вызов функций-задач осуществляется только самим планировщиком и скрыт от программиста;
  3. вызов функций из тела прерывания - не всеми поддерживаемая практика, тем не менее, такое допустимо.

После ресета мы попадаем в функцию main(), где производим инициализацию системы OS_Init(), создаем задачи OS_Task_Create() и передаем управление планировщику OS_Run(). Планировщик живет сам по себе, циклически проверяя все задачи в своем внутреннем списке на предмет готовности. Когда появляются готовые к выполнению задачи, планировщик выбирает из них ту, приоритет которой самый высокий, и передает ей управление. Обратите внимание, что передача управления задаче отмечена на рисунке темно-красной линией: это значит, что задача должна будет сама вернуть управление ядру, иначе остальные задачи никогда не получат управление. Для возврата в планировщик есть специальные сервисы: безусловный возврат, перевод в режим ожидания события или задержки, приостановка задачи и удаление задачи.

Во время своего выполнения задача может вызывать другие функции (не являющиеся задачами).

Обмен данными

Голубыми линии показано направление обмена данными. Рассмотрим особенности:

  • OS_Init() очищает массив задач;
  • OS_Task_Create() - добавляет задачу в список, если там есть свободное место;
  • массив задач снабжает данными планировщик;
  • OS_Timer() увеличивает значения таймеров задач, отслеживая их переполнение, и пользовательских таймеров, также следя за переполнениями;
  • из функций задач, помимо обычных сервисов, разрешенных для использования в любом месте программы, можно также использовать сервисы, переключающие контекст (т.е. передающие управление планировщику), поэтому рядом с линией между задачами и сервисами стоит литера "Т";
  • из прерывания, помимо общих сервисов, можно вызывать специальные сервисы, предназначенные для работы в прерывании, поэтому рядом с линией стоит литера "I";
  • из функций разрешен вызов сервисов, не переключающих контекст и не предназначенных для работы в прерывании.

Особенности программы, написанной с использованием OSA

Программа, написанная с использованием ОСРВ OSA, должна быть написана с учетом некоторых требований:

  1. в папке проекта должен быть размещен файл osacfg.h - файл конфигурации системы (в простейшем случае этот файл может быть пустым);
  2. в проект, помимо файлов самой программы, должен быть включен файл osa.c (это требование не относится к программам, написанным на CCS);
  3. во все Си-файлы проекта, в которых предполагается использование системных сервисов, должен быть включен файл osa.h (директивой #include);
  4. в опциях компилятора должны быть указаны пути для включаемых файлов (include-пути): путь к папке проекта и путь к папке, где находятся файлы osa.c и osa.h;
  5. в функции main() должны вызываться два системных сервиса: OS_Init и OS_Run.
  6. в программе должна быть одна или несколько функций, описанных как задачи.

Ниже в двух словах все эти пункты расшифровываются. Здесь их расшифровка приводится только для информации; более подробно они будут рассмотрены по ходу уроков.

osacfg.h

Система OSA является довольно гибкой в настройке, что позволяет сконфигурировать ее под конкретный проект так, чтобы минимизировать ресурсозатраты. Все указания по конфигурации OSA для конкретного проекта задаются в файле osacfg.h (поэтому и важно, чтобы файл находился именно в папке проекта). В самом простом случае этот файл может быть пустым, тогда система будет сконфигурирована по умолчанию. Однако в большинстве случаев может понадобиться внести коррекцию в настройки системы с тем, чтобы повысить эффективность использования ОСРВ.

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

osa.c

Этот файл содержит определения всех системных переменных и функций. Было решено его оставить именно в виде Си-файла, не компилируя в библиотеку, с тем, чтобы он был максимально гибким в настройке. Т.к. PIC-контроллеры младшего семейства имеют дефицит ресурсов, то бывает так, что каждый байт на счету. Максимальной оптимизации получится добиться только использованием условных директив в Си-файле. В противном случае пришлось бы создавать целую кучу вариантов библиотечных файлов под разные контроллеры и под различные конфигурации, что усложнило бы процесс создания проекта и процесс изменения конфигурации в ходе работы над проектом.

osa.h

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

include-пути

Здесь все просто: компилятор должен знать, где искать подключаемые файлы, а именно - osa.h (и, следовательно, все файлы включаемые в него из папок osa\port и osa\service) и файл osacfg.h, чтобы он был включен в osa.h.

OS_Init() и OS_Run()

Наличие вызова этих двух сервисов обязательно. Причем OS_Init должен быть вызван в самом начале программы до вызова какого-либо другого сервиса ОС, а OS_Run - в самом конце (после OS_Run ничего не будет выполняться, т.к. этот сервис переводит программу в вечный цикл).

OS_Init - процедура инициализации системы. В ней подготавливаются все системные переменные, обнуляются списки задач, таймеров, семафоров. Этот сервис должен вызываться один раз за все выполнение программы (если, конечно, самой программой не предусмотрен программный ресет).

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

void main (void)
{
    ...           // Инициализация периферии, переменных программы и пр.
    OS_Init();
    ...           // Создание задач, подготовка сервисов
    OS_Run();
}

Функции-задачи

Те функции, которые предполагается выполнять параллельно, т.е. сами задачи, описываются как обычные Си-функции с параметром void, но должны быть оформлены по определенным правилам:

  1. тело функции должно содержать вечный цикл (есть случаи, когда можно обойтись и без него, но на первых порах этого делать не стоит);
  2. внутри вечного цикла должен вызываться хотя бы один сервис, передающий управление планировщику;
  3. из этой функции нельзя осуществлять выход оператором return.

Из функции-задачи можно вызывать другие функции (не являющиеся задачами). Локальные переменные всех функций задач занимают одну и ту же область памяти, и после передачи задачей управления планировщику значения локальных переменных будут утеряны. Если есть переменные, значения которых важно сохранить до следующего получения управления, должны быть описаны как static.

Вот типичный пример функции-задачи:

void Task_1 (void)
{
    static unsigned char s_ucCounter;    // Определение глобальных переменных задачи
           unsigned char i, j;           // Определение локальных переменных задачи
 
    s_ucCounter = 0;                     // Код подготовки (если нужен)
 
    for (;;)
    {
        OS_Yield();
    }
}

Осталось только определиться с тем, как операционная система узнает, какие функции являются задачами, а какие - нет. Мы сами ей об этом сообщим через системный сервис OS_Task_Create, добавив, таким образом, указатель на функцию во внутренний список системы. Сервис OS_Run в своем цикле просматривает этот список и передает управление по занесенным в него указателям.

void main (void)
{
    OS_Init();
    OS_Task_Create(0, Task_1);    // Теперь система знает, что Task_1 - это задача ОС
    OS_Run();
}

Замечание об именах идентификаторов

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

  • имена констант будут даваться заглавными буквами;
  • имена переменных будут содержать префиксы:
    1. области видимости:
      • "m_" - глобальная для всех модулей;
      • "s_" - статическая переменная внутри функции;
      • без префикса - локальная переменная внутри функции или аргумент функции.
    2. типа переменной:
      • "uc" - unsigned char;
      • "sc" - signed char;
      • "c" - char (в случаях, когда не важно, знаковый или беззнаковый);
      • "b" - bit;
      • "n" - signed int;
      • "w" - unsigned int;
      • "l" - signed long;
      • "dw" - unsigned long;
      • "smsg_", "msg_", "queue_" и т.п. - префиксы для глобальных переменных управления сервисами.
  • имена функций-задач будут содержать префикс "Task_".

Такая система имен может показаться запутанной и ненужной, однако моя практика показала, что она позволяет, во-первых, предотвращать ошибки на стадии написания программы, а во-вторых, сильно упрощает чтение чужой программы (или своей спустя год-два). Возможно, у кого-то есть своя система имен, я никому мою не навязываю, но в данных уроках мы будем пользоваться приведенной выше системой. И встретив в программе переменную с именем, например, s_ucCounter, вы сразу поймете, что это статическая переменная внутри задачи (ее значение будет сохраняться после передачи управления планировщику) типа unsigned char.

Замечание о локальных переменных внутри функций-задач

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

Однако в случае с ОСРВ дело обстоит иначе, а именно - все функции-задачи, находящиеся на одном уровне в графе вызовов (первый уровень после main), выполняются параллельно, постоянно сменяя друг друга в ходе выполнения. И т.к. у них под локальные переменные задействована одна и та же область памяти, задачи при переключении между собой будут пользоваться этой областью каждая под свои нужды, затирая данные, записанные туда другими задачами. Поэтому, если важно, чтобы значение переменной сохранилось после передачи управления планировщику (а следовательно - и другим задачам), то ее нужно определять с квалификатором static. Тогда эта переменная будет размещена в другой области памяти и будет закреплена исключительно за той функцией, в которой она определена. По ходу рассмотрения уроков мы будем возвращаться к этой теме.

Состояния задач

Как описано в документации в параграфе "Состояния задач", задача может находиться в одном из пяти состояний: не создана (удалена), в ожидании, в готовности, в работе и приостановлена. Ниже схематически изображен граф изменения состояний:

Примечания:

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

Особенности отладки

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

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

Пошаговая отладка

Два слова о том, как работает пошаговый отладчик (это относится и к симулятору, и к внутрисхемным отладчикам). При выполнении команды Step Over (F8) отладчик делает следующее:

  • запоминает адрес следующей за выполняемой строкой строки (при работе в симуляторе он просто запоминается, при работе с внутрисхемными отладчиками на следующей строке устанавливается breakpoint);
  • запускает программу на выполнение;
  • дойдя до запомненного адреса (или до breakpoint'а) останавливает выполнение программы;
  • при работе с внутрисхемными отладчиками удаляет breakpoint.

Зачем такие хитрости с точками останова? Дело в том, что одна строка Си-кода может быть транслирована в несколько (иногда в сотни) инструкций ассемблера. Кроме того, текущая строка может просто-напросто содержать в себе вызов подпрограммы. Чтобы при выполнении каждой инструкции не проверять, дошла ли программа до требуемого адреса, ставится точка останова. К слову сказать, симулятору все равно, как это делать: через точку останова или прямым сравнением текущего адреса с адресом следующей строки; но внутрисхемному отладчику это важно, т.к. проверка адреса после выполнения каждой инструкции очень сильно замедлила бы его работу.

Рассмотрим на конкретном примере. Предположим, программный счетчик в данный момент указывает на строку программы, содержащую вызов функции:

Когда пользователь нажимает F8, запоминается программный адрес следующей за вызовом функции delay(10) строки: "cCounter = 25;". Далее программа начинает выполняться. Т.е. заходит внутрь подпрограммы delay(), выполняет внутри нее какие-то действия, возможно даже вызов других подпрограмм. Но как только будет произведен возврат из подпрограммы delay(), и программный счетчик станет равным запомненному в начале адресу (или, если мы работаем с внутрисхемным отладчиком, как только программа дойдет до точки останова), отладчик приостановит выполнение программы (удалив при этом точку останова), и мы увидим, что зеленая стрелка теперь указывает на строку присваивания "cCounter = 25;".

Примечание. Если в теле функции delay() или в теле одной из функций, которые вызываются из функции delay(), стоит пользовательская точка останова, то отладчик прервет выполнение программы, как только программный счетчик станет равным адресу точки останова.

Рассмотрим простой пример синхронизации двух задач с помощью бинарного семафора:

void Task1 (void)
{
    for (;;)
    {
        OS_Bsem_Set(0);      // Позволяем выполниться задаче 2
        OS_Delay(10);
    }
}
//--------------------------
void Task2 (void)
{
    for (;;)
    {
        OS_Bsem_Wait(0);     // Ждем события от задачи 1
        PIN_LED ^= 1;            // Мигаем светодиодом
    }
}

Примечание: PIN_LED - вывод контроллера, управляющий светодиодом.

Эта программа включает/выключает светодиод на выводе PIN_LED с интервалом в 10 системных тиков. Логика работы программы такова: задача Task1 с интервалом в 10 системных тиков устанавливает бинарный семафор. Задача Task2 находится в режиме ожидания семафора. Как только она видит, что семафор установлен, она становится готовой к выполнению и при первой же возможности получает его (становится в режим работы), при этом семафор сразу сбрасывается (это делается автоматически сервисом OS_Bsem_Wait()). После этого состояние светодиода меняется на противоположное (зажжен/погашен), и задача вновь становится в режим ожидания двоичного семафора (ведь он был сброшен сразу же при входе в задачу). Задача Task1 все это время находится в режимы ожидания задержки. Через 10 системных тиков она станет готовой к выполнению и после его получения вновь установит семафор.

Схематически программа будет выглядеть так:

Как видно из рисунка, обе задачи выполняются по одному разу, после чего система бездействует (планировщик крутится впустую, т.к. нет готовых задач: одна ждет завершения задержки, другая - установки семафора). В это время у нас происходят прерывания, в коде обработчика которых вызывается сервис OS_Timer, который следит за отсчетом задержки задачи Task1. Как только время задержки истекает, планировщик переводит задачу Task1 в готовность, а так как она единственная готовая к выполнению задача, то она сразу получает управление. Получив его, она вновь устанавливает семафор, делая готовой задачу Task2, а сама опять встает в режим ожидания задержки. И так по кругу.

Рассмотрим, что произойдет, если мы попытаемся пройти эту программу в симуляторе по шагам (F8).

Ниже будет рассмотрено поведение отладчика при работе под PICC18, MCC18 и MCC30. При отладке под PICC для контроллеров PIC10, PIC12 и PIC16 есть свои нюансы, которые будут рассмотрены далее.

Первым делом мы попадаем в начало функции main(). Пройдя по шагам вызовы всех подпрограмм инициализации (периферии и системы) и создание задач, мы подойдем к вызову сервиса OS_Run, который запускает планировщик в работу. Здесь мы столкнемся с первой неприятностью. Дело в том, что при попытке выполнить OS_Run командой Step Over (F8) программа начнет выполняться и никогда не остановится (если мы ее сами не остановим). Произойдет это потому, что отладчик запомнит адрес следующей за OS_Run строки, но на него программа никогда не попадет, т.к. OS_Run содержит в себе вечный цикл.

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

Task1

Итак, попробуем выполнить задачу Task1 по шагам, для чего сперва установим точку останова на вызове сервиса:

Далее хоть по шагам (F8), хоть сразу (F9) - дойдем до этой точки останова. Теперь рассмотрим, что получится, когда мы попытаемся выполнить пошаговую отладку. Если бы программа была написана по принципу суперцикла, то поведение отладчика было бы предсказуемым: после установки семафора мы бы через несколько шагов очутились бы в задаче Task2 на строке, следующей за ожиданием семафора. Однако с программой, написанной под ОСРВ, отладчик поведет себя немного иначе. Нажмем F8, выполним тем самым вызов сервиса OS_Bsem_Set(), т.е. установив двоичный семафор. Отладчик сделает шаг и остановится перед вызовом системного сервиса OS_Delay(). Сервис OS_Bsem_Set не содержит внутри себя передачи управления планировщику, поэтому с ним трудностей не возникло. Но вот сервис OS_Delay при выполнении передаст управление планировщику.

Что при этом произойдет. Отладчик запомнит адрес следующей строки за OS_Delay, т.е. адрес конца (или начала, как удобнее) цикла, и запустит программу на выполнение. Программа будет выполняться до тех пор, пока программный счетчик не станет равным запомненному адресу. Т.е. за один шаг F8 произойдет следующее:

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

Другими словами вся работа Task2 выполнится незаметно для нас. Мы нажмем F8, находясь перед вызовом OS_Delay() и имея установленный семафор, и сразу же окажемся в начале цикла, перед вызовом сервиса OS_Bsem_Set(). К этому моменту мы обнаружим, что светодиод уже поменял свое состояние, а семафор уже сброшен. Схематически это можно изобразить так:

Все, что в заштрихованной области, включая все тело Task2, будет выполнено за один шаг.

Task2

Точно так же отладчик поведет себя, если мы попытаемся выполнить по шагам задачу Task2. Дойдя до вызова сервиса OS_Bsem_Wait, который передает управление планировщику, мы будем знать, что при выполнении команды отладчика Step Over (F8) мы за один шаг пропустим весь цикл выполнения задачи Task1, вместе с задержкой в 10 системных тиков.

Все, что в заштрихованной области, включая все тело Task1, будет выполнено за один шаг.

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

Вывод

К чему все это рассказывается? Вот к чему:

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

В нашем конкретном примере нужно поставить две точки останова: за сервисом OS_Delay (а именно - на вызове сервиса OS_Bsem_Set) для задачи Task1 и за сервисом OS_Bsem_Wait (а именно - на операции PIN_LED ^= 1) для задачи Task2.

Примечание для PIC10, PIC12 и PIC16

Все выше сказанное относилось к работе с компиляторами PICC18, MCC18 и MCC30. При работе с компилятором PICC нельзя точно предсказать поведение отладчика при попытке выполнить сервис, переключающий контекст, в пошаговом режиме. Поэтому, когда будете производить отладку программы, написанной под PICC, выполняйте сервисы, переключающие контекст, командой отладчика Run (F9), предварительно установив точки останова в интересующих местах. Иногда это бывает сложно (особенно при работе с внутрисхемным отладчиком, который имеет возможность поставить всего одну точку останова), т.к. не всегда удается предсказать, какую задачу планировщик выберет для выполнения. Но другого способа отладки пока нет.

Просмотр переменных

Работая с сервисами, управляющими пользовательскими данными (сообщения, очереди, флаги, семафоры, таймеры), иногда необходимо следить за их состояниями через окно Watch. С переменными, создаваемыми программистом явно, все понятно: каждая такая переменная имеет свой идентификатор и свою область видимости. Например:

OST_SMSG   smsg_Sound;
OST_DTIMER dt_Alarm;

Эти две переменные типов "короткое сообщение" и "динамический таймер" могут быть добавлены в окно Watch, в котором за ними можно будет наблюдать. Таким же образом можно наблюдать за переменными следующих типов:

  • OST_SMSG - короткое однобайтовое сообщение;
  • OST_MSG - указатель на сообщение;
  • OST_MSG_CB - дескриптор управления указателем на сообщение;
  • OST_FLAG, OST_FLAG16, OST_FLAG32) - флаги;
  • OST_CSEM - счетный семафор;
  • OST_QUEUE, OST_SQUEUE - очереди сообщений;
  • OST_DTIMER - динамический таймер.

Но OSA имеет два вида пользовательских переменных, которые создаются на этапе компиляции и контролируются самой системой:

  • двоичные семафоры;
  • статические таймеры.

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

Как их наблюдать?

Двоичные семафоры

Все двоичные семафоры хранятся в массиве:

unsigned char OS_BSem[];

размерность которого зависит от количества используемых в системе семафоров (задается константой OS_BSEMS в файле конфигурации osacfg.h) из расчета: один байт на каждую неполную восьмерку. (Примечание. Для MCC30 элементы массива имею тип unsigned int.)

Младший бит в нулевом элементе массива - это нулевой семафор. Байт с маской 0x02 - первый, 0x04 - второй и т.д. Таким образом, чтобы отследить нужный нам семафор при отладке, нам надо следить за состоянием соответствующего бита в массиве OS_BSem. Это довольно неудобно, но такова плата за экономию оперативной памяти.

Статические таймеры

Все статические таймеры хранятся в массиве:

_OST_STIMER OS_STimers[];

Тип элементов массива и размерность массива задается в файле osacfg.h константами OS_STIMER_SIZE и OS_STIMERS, соответственно. Каждый элемент массива - отдельный таймер. Самый старший бит каждого таймера (7-ой, 15-ый или 31-ый, в зависимости от типа) - бит активности. Если он установлен, значит, таймер считает. Если сброшен, - таймер переполнился и остановлен. Статические таймеры в окне Watch удобно отображать в виде знаковых целых чисел. Тогда будет видно, сколько времени осталось до окончания счета. Например:

Здесь видно, что статический таймер 0 считает (т.к. число отрицательное), и ему осталось 2000 системных тиков до завершения счета. Статический таймер 1 - не считает, т.к. старший бит сброшен (число не отрицательное).

 
osa/tutorial/introduction.txt · Последние изменения: 19.01.2011 11:25 От osa_chief
 
Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki