FreeRTOS — операционная система для микроконтроллеров. Часть 10. Отладка приложения.


Автор: Андрей Курниц (kurnits@stim.by). Журнал КиТ
Эта статья продолжает знакомить читателя с созданием программ, ра¬ботающих под управлением FreeRTOS — операционной системы для микроконтроллеров. на этот раз внимание будет уделено этапу отладки приложения: мы рассмотрим возможности FreeRTOS, которые помогают найти трудно выявляемые ошибки в программе, определить узкие места программы и оценить пути ее дальнейшего расширения.

Возможности трассировки

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

Последняя версия FreeRTOS (на момент написания статьи — V7.0.1) поддерживает два способа трассировки выполняющейся программы:

  1. Механизм трассировки с записью в буфер (Legacy Trace Utility).
  2. С использованием макросов трассировки (Trace Hook Macros).

Трассировка с записью в буфер

Такой метод трассировки заключается в том, что информация о последовательности выпол­нения задач во FreeRTOS и времени выполнения каждой задачи записывается в специально отве­денный для этого буфер в оперативной памяти. Далее одну из задач можно запрограммировать так, чтобы она выводила содержимое буфера с трассировочной информацией через какой­либо интерфейс (RS-232C, например) или запи­сывала в ПЗУ для дальнейшего изучения.

Чтобы возможность трассировки с за­писью в буфер была доступна, необходи­мо установить значение макроопределе­ния configUSE_TRACE_FACILITY в файле FreeRTOSConfig.h равным 1. (Значение «0», соответственно, исключает механизм трасси­ровки с записью в буфер из ядра FreeRTOS.)

Для того чтобы начать запись информации о работе ядра FreeRTOS, следует вызвать API­функцию vTaskStartTrace(). Ее прототип:

void vTaskStartTrace( portCHAR * pcBuffer, unsigned portLONG ulBufferSize );

Аргументы:

1. pcBuffer — указатель на буфер, в который бу­дет записана трассировочная информация.

2. ulBufferSize — задает предельный размер буфера. Когда в буфер будет записано ulBufferSize байт информации, трассиров­ка будет автоматически остановлена. Трассировка может быть остановлена

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

unsigned portLONG ulTaskEndTrace( void );

API-функция ulTaskEndTrace() возвраща­ет фактическое количество байт трассиро­вочной информации, которая была записана в буфер.

Информация в буфере записывается в би­нарном виде. Утилита TraceCon позволяет конвертировать бинарное содержимое буфера в удобочитаемую текстовую форму. Утилита TraceCon входит в дистрибутив FreeRTOS (располагается в директории /TraceCon) и вы­полняется в среде DOS или Windows.

FreeRTOS портирована на различные ар­хитектуры, среди которых есть процессоры с различным порядком следования байтов [3]. Поэтому в директории /TraceCon нахо­дятся две версии утилиты TraceCon:

  1. tracecon_little_endian.exe — предназначена для процессоров с порядком следования от младшего к старшему («интеловский» порядок байтов).
  2. tracecon_big_endian_untested.exe — предна­значена для процессоров с порядком следо­вания от старшего к младшему («сетевой», или «мотороловский» порядок байтов). Порядок работы с утилитой TraceCon сле­дующий. Двоичное содержимое буфера с трас­сировочной информацией необходимо ско­пировать в файл TRACE.BIN. Далее этот файл нужно поместить в одной директории вместе с утилитой TraceCon. После чего ее необхо­димо выполнить. В результате утилита соз­даст в текущей директории файл TRACE.TXT, содержащий трассировочную информацию в текстовой форме.

Текстовый файл с трассировочной инфор­мацией содержит записи вида:

Пример фрагмента текстового трассировочного файла:
5012.450000 56 5012.500000 56 5012.500000 57 5012.550000 57 5012.550000 58 5012.600000 58 5012.600000 63 5013.000000 63 5013.000000 66 5013.050000 66 5013.050000 0 5013.100000 0 5013.100000 1 5013.150000 1 5013.150000

Графа «Время» определяет, сколько вре­мени прошло с момента запуска планиров­щика, когда задача с номером («Номер зада­чи») получила управление.

Время в трассировочном файле представ­лено в системных квантах. Однако переклю­чение контекста может происходить чаще, чем частота следования системных кван­тов (например, при блокировании задачи). Поэтому при переключении контекста в те­чение системного кванта время может быть записано не точно, но последовательность записей в трассировочном файле точно отра­жает последовательность выполнения задач в программе. Тем не менее для конкретного порта FreeRTOS точность представления вре­мени в трассировочном файле может быть повышена за счет использования текущего значения таймера/счетчика, который отсчи­тывает системные кванты времени. В при­веденном примере точность представления времени составляет 0,05 кванта времени.

Второй графой в трассировочном файле выступает номер задачи. Он уникален для каждого экземпляра каждой задачи. Номер за­дачи является составной частью блока управ­ления задачей (Task Control Block) и использу­ется только для трассировки. Если трассировка с записью в буфер отключена (макроопределе­ние configUSE_TRACE_FACILITY равно «0»), то номер задачи автоматически исключается из блока управления задачей.

Получить номер задачи можно при помощи API-функции vTaskList(), которая выводит спи­сок всех задач в программе. Когда выполняется API-функция vTaskList(), прерывания процес­сора запрещены, то есть программа находится в критической секции. Отлаженная программа не должна содержать вызовов vTaskList(), при­менять ее следует только для отладки.

Пример списка задач, полученный с помо­щью вызова API-функции vTaskList():

Name   State|Priority|Stack|Num
*******************************
Print    R 4 358 64
QConsB6  R 0 192 58
Rec3     R 0 176 63
C_CTRL   R 0 193 31
IDLE     R 0 212 66
Math1    R 0 442 0
QConsNB  B 2 190 11
BTest1   B 7 195 19
FBTest1  B 3 193 51
QConsNB  B 2 192 59
QProdNB  B 2 190 12
QProdNB  B 2 192 60
PeekM    S 1 204 42
Event2   S 3 194 39
MuHigh   S 3 196 24
MuMed    S 2 218 23
Event3   S 3 194 40
FMuHigh  S 3 206 50
FMuMed   S 2 218 49
COMRx    S 3 185 9

Список задач содержит следующие поля:
1. Name. Имя задачи, которое было назначе¬но ей в момент создания (второй аргумент API-функции xTaskCreate()).
2. State. Состояние задачи в момент вызова vTaskList(). Может принимать следую¬щие значения:
• «R» — Ready, состояние готовности к выполнению.
• «B» — Blocked, блокированное состояние задачи.
• «S» — Suspended, приостановленное состояние.
• «D» — Deleted. Означает, что задача была удалена.
• 3. Priority. Приоритет задачи в момент вызова vTaskList().
• 4. Stack. Максимальный объем стека, который использовала задача за время своей работы.
• 5. Num. Уникальный номер задачи в системе. Так как может быть создано несколько экземпляров одной задачи, то поле Name может повторяться в списке (QProdNB в примере выше), однако номер задачи уникален для каждого ее экземпляра.

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

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

Как и при выполнении API-функции vTaskStartTrace(), список задач помещается в предварительно подготовленный буфер в оперативной памяти. Список в буфере представлен непосредственно в текстовой форме, как и показано в примере выше.

Прототип vTaskList():

void vTaskList( portCHAR *pcWriteBuffer );

Единственный аргумент pcWriteBuffer — указатель на буфер, в который будет помещен список задач втекстовом виде. Предполагается, что размер буфера достаточен для размещения всего списка. Необходимый размер буфера можно вычислить приблизительно, приняв, что для представления информации об одной задаче требуется около 40 байт.

Чтобы API-функция vTaskList() была до­ступна, необходимо, чтобы макроопределе­ния configUSE_TRACE_FACILITY, INCLUDE_ vTaskDelete и INCLUDE_vTaskSuspend в фай­ле FreeRTOSConfig.h были равны 1.

Оценить возможности трассировки с за­писью в буфер можно на примере демон­страционного проекта для порта FreeRTOS для x86-процессора, работающего в реаль­ном режиме (именно этот порт использовал­ся в большинстве учебных программ в пред­ыдущих публикациях [1]).

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

Прежде всего необходимо задать ме­сто расположения трассировочного файла и файла со списком задач, так как изначально их место расположения задано как корневая директория диска A:. Сделать это можно, отредактировав файл fileIO.c, находящийся в директории \Demo\PC\FileIO\. Строку:

const char * const pcFileName = "a:\\RTOSlog.txt";

необходимо заменить на:

const char * const pcFileName = "c:\\RTOSlog.txt";

а строку:

const char * const pcFileName = "a:\\trace.bin";

следует заменить на строку:

const char * const pcFileName = "c:\\trace.bin";

Таким образом, трассировочный файл и файл со списком задач будут созданы в корневой директории диска C:\. Если нуж­но выбрать другое место расположения фай­лов, то нужно задать соответствующий путь в строках, приведенных выше.

Далее следует выполнить сборку де­монстрационного проекта в среде Open Watcom IDE. Как установить и настроить эту среду, рассказано в [1, № 4]. Обязательным условием для того, чтобы трассировка ра­ботала, является сборка демонстрационно­го проекта в Release-конфигурации. Однако изначально выбрана отладочная Debug­конфигурация, в которой возможность трас­сировки отключена.

Для того чтобы переключить конфигура­цию проекта на Release, необходимо в сре­де Open Watcom IDE выбрать пункт меню Targets Target Options Use Release Switches (рис. 1).

Кроме того, необходимо убедиться, что возможность трассировки включена в кон­фигурационном файле FreeRTOSConfig.h, который должен содержать строку:

#define configUSE_TRACE_FACILITY 1

Когда все вышеперечисленные условия выполнены, можно выполнить сборку де­монстрационного проекта и запустить по­лученный исполняемый *.exe файл на вы­полнение.

Получить файл со списком задач можно, если во время выполнения демонстрацион­ного проекта нажать «t» на клавиатуре. При этом в корневой директории диска C: появится файл RTOSlog.txt, содержащий список всех задач в системе.

Начать трассировку можно, нажав «s» на клавиатуре, а чтобы закончить — клави­шу «e». При этом в корневой директории диска C: появится трассировочный файл Trace.bin, содержащий трассировочную ин­формацию в бинарном виде.

Для того чтобы получить текстовый файл с трассировочной информацией, необходи­мо скопировать полученный файл Trace.bin в одну директорию с утилитой tracecon_ little_endian.exe и запустить ее. В результате в этой же директории будет создан готовый для анализа текстовый трассировочный файл Trace.txt.

Графическое представление трассировочной информации

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

Получить графическое представление ин­формации из текстового трассировочного файла можно различными способами. Один из них, который будет рассмотрен ниже, — с помощью редактора электронных таблиц Microsoft Excel. Рассматривается работа с рус­ской версией Microsoft Excel 2007/2010.

В главном меню программы необходимо выбрать закладку «Данные», далее «Получить внешние данные» «Из текста». После это­го «мастер» предложит выбрать файл — источник данных, в качестве которого следует выбрать полученный ранее текстовый трас­сировочный файл. После импортирования данных первый столбец таблицы должен со­держать числовое значение времени, а вто­рой — номера задачи. Каждая строка табли­цы будет соответствовать факту переключе­ния процессора на другую задачу.

Далее следует выделить интересующий участок полученной таблицы и выбрать вкладку «Вставка», затем «Диаграммы» «Точечная» «Точечная с прямыми отрез­ками». В результате мы получим график, по­добный представленному на рис. 2.

Трассировка с помощью макросов трассировки

Начиная с версии V4.8.0 во FreeRTOS поя­вился более универсальный, чем трассиров­ка с записью в буфер, механизм трассиров­ки — макросы. Макросы трассировки — это инструмент, позволяющий получать расши­ренную информацию о поведении програм­мы, работающей под управлением FreeRTOS.

Макросы трассировки дают возможность:

  1. Записать последовательность выполнения задач.
  2. Измерить время выполнения каждой зада­чи.
  3. Зафиксировать факт наступления и время, когда происходят определенные события ядра или вызовы API-функций.
  4. Получать информацию только об опреде­ленных событиях, связанных с определен­ными задачами или очередями, и оставить без внимания остальную активность ядра. Главная «изюминка» реализации макросов трассировки — это то, что исходный код са­мой FreeRTOS содержит вызовы изначально пустых макросов, которые включены во все основные операции внутри ядра: переклю­чение контекста, вызов определенной API­функции и т. д. Макросы трассировки могут быть переопределены в программе для вы­полнения любых предусмотренных про­граммистом действий. Обычно эти действия направлены на какую-либо индикацию, ото­бражение и, возможно, запись факта вызова того или иного макроса трассировки.

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

Приведем несколько примеров, как можно реализовать индикацию переключения задач в макросах трассировки:

  1. Использование нескольких цифровых вы­ходов микроконтроллера (МК) для инди­кации номера выполняемой в данный мо­мент задачи. Номер задачи можно задавать, например, в двоичном или позиционном коде. К этим цифровым выходам может быть подключен цифровой анализатор для записи и дальнейшего анализа последова­тельности выполнения задач.
  2. Использование одного аналогового выхо­да, при этом аналоговый уровень напряже­ния будет индицировать выполняющую­ся в данный момент задачу. Этот способ подобен первому, но вместо цифрового анализатора для записи трассировочной информации потребуется обычный осцил­лограф. Макросы трассировки можно использо­вать для интеграции с отладчиками сторон­них производителей.

Тэг задачи

Тэг задачи подобен рассмотренному выше номеру задачи и представляет собой допол­нительный параметр, который включается в состав блока управления задачей, если ма­кроопределение configUSE_APPLICATION_ TASK_TAG равно 1.

Тэг задачи определен как переменная типа pdTASK_HOOK_CODE. Тип pdTASK_ HOOK_CODE определен как указатель на функцию:

typedef portBASE_TYPE (*pdTASK_HOOK_CODE)( void * );

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

Для того чтобы назначить задаче опре­деленный тэг, служит API-функция vTaskSetApplicationTaskTag(). Ее прототип:

void vTaskSetApplicationTaskTag( xTaskHandle xTask, pdTASK_ HOOK_CODE pxTagValue );

Аргумент xTask служит для задания де­скриптора задачи, тэг которой необходимо задать. Аргумент pxTagValue непосредствен­но определяет само значение тэга.

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

Рассмотрим пример использования тэга задачи в макросах трассировки. В программе двум задачам будут назначены тэги, равные, со­ответственно, 1 и 2. Далее будет переопределен трассировочный макрос traceSWITCHED_IN() (вызывается каждый раз при переключении на следующую задачу) так, чтобы он выводил значение тэга текущей задачи на вывод микро­контроллера:

/* Функция, реализующая Задачу 1 */
void vTask1( void *pvParameters )
{
/* Задать для этой задачи тэг, равный 1 */
vTaskSetApplicationTaskTag( NULL, ( void * ) 1 );
for( ;; )
{
/* Полезный код */
}
}
/*************************************************/
/* Функция, реализующая Задачу 2 */
void vTask1( void *pvParameters )
{
/* Задать для этой задачи тэг, равный 2 */
vTaskSetApplicationTaskTag( NULL, ( void * ) 2 );
for( ;; )
{
/* Полезный код */
}
}
/*************************************************/
/* Переопределить трассировочный макрос traceTASK_SWITCHED_IN() так, чтобы он устанавливал аналоговое напряжение на выводе пропорционально тэгу задачи, которая вызвала этот макрос. */ #define traceTASK_SWITCHED_IN() vSetAnalogueOutput( 0, ( int ) pxCurrentTCB->pxTaskTag )

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

/* traceBLOCKING_ON_QUEUE_RECEIVE() — один из макросов,
позволяющий определить причину переключения
с одной задачи на другую. */
#define traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue) \
ulSwitchReason = reasonBLOCKING_ON_QUEUE_READ;
/* log_event() — это функция, определенная программистом,
которая получает через свои аргументы задачу, которая вышла
из состояния выполнения, и причину, почему это произошло. */
#define traceTASK_SWITCHED_OUT() \
log_event( pxCurrentTCB, ulSwitchReason );

Макросы, которые вызываются из прерываний, а в частности — из прерывания таймера/счетчика, который отсчитывает систем¬ные кванты времени, должны выполняться как можно быстрее. Операции «установка значений переменных», «запись в порт ввода/вывода или регистр специальных функций МК» допустимы, напротив, попытка записи отладочного сообщения с помощью fprintf() не будет работать.

Таблица. Список макросов трассировки

идентификатор макроса трассировки Описание
traceTASK_INCREMENT_TICK(xTickCount) Вызывается из тела прерывания таймера/счетчика, который отсчитывает системные кванты времени. Параметр: текущее значение счетчика системных квантов
traceTASK_SWITCHED_OUT() Вызывается перед тем, как задача выйдет из состояния выполнения и управление получит другая задача. При этом глобальная переменная pxCurrentTCB содержит дескриптор задачи, которую вытеснит другая задача
traceTASK_SWITCHED_IN() Вызывается в момент, когда задача переходит в состояние выполнения. При этом глобальная переменная pxCurrentTCB содержит дескриптор этой задачи
traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue) Индицирует, что текущая задача переходит в блокированное состояние, когда она сделала попытку: – прочитать из пустой очереди; – захватить уже захваченный семафор или мьютекс. Параметр: дескриптор очереди, семафора или мьютекса
traceBLOCKING_ON_QUEUE_SEND(pxQueue) Индицирует, что текущая задача переходит в блокированное состояние, когда она сделала попытку записи в полностью заполненную очередь. Параметр: дескриптор очереди
traceGIVE_MUTEX_RECURSIVE(pxMutex) Вызывается, когда API-функция xSemaphoreGiveRecursive() пытается освободить мьютекс. Параметр: дескриптор мьютекса
traceGIVE_MUTEX_RECURSIVE_FAILED(pxMutex) Вызывается, когда попытка освободить мьютекс с помощью API-функции xSemaphoreGiveRecursive() оказывается неуспешной. Параметр: дескриптор мьютекса
traceQUEUE_CREATE(pxNewQueue) Вызывается из API-функции xQueueCreate(), если очередь успешно создана. Параметр: дескриптор очереди
traceQUEUE_CREATE_FAILED() Вызывается из API-функции xQueueCreate(), если очередь не создана из-за отсутствия достаточного объема памяти. Параметр: дескриптор очереди
traceCREATE_MUTEX(pxNewMutex) Вызывается из API-функции xSemaphoreCreateMutex(), если мьютекс успешно создан. Параметр: дескриптор мьютекса
traceCREATE_MUTEX_FAILED() Вызывается из API-функции xSemaphoreCreateMutex(), если мьютекс не создан (мало памяти)
traceGIVE_MUTEX_RECURSIVE(pxMutex) Вызывается из API-функции xSemaphoreGiveRecursive(), если мьютекс успешно возвращен. Параметр: дескриптор мьютекса
traceGIVE_MUTEX_RECURSIVE_FAILED(pxMutex) Вызывается из API-функции xSemaphoreGiveRecursive(), если мьютекс не возвращен из-за того, что вызывающая задача не захватила мьютекс ранее (не является его владельцем). Параметр: дескриптор мьютекса
traceTAKE_MUTEX_RECURSIVE(pxMutex) Вызывается из API-функции xQueueTakeMutexRecursive(). Параметр: дескриптор мьютекса
traceCREATE_COUNTING_SEMAPHORE() Вызывается из API-функции xSemaphoreCreateCounting(), если семафор успешно создан
traceCREATE_COUNTING_SEMAPHORE_FAILED() Вызывается из API-функции xSemaphoreCreateCounting(), если семафор не создан (мало памяти)
traceQUEUE_SEND(pxQueue) Вызывается из API-функций xQueueSend(), xQueueSendToFront(), xQueueSendToBack() или из любой API-функции, которая возвращает семафор, если операция записи в очередь была успешно выполнена. Параметр: дескриптор очереди или семафора
traceQUEUE_SEND_FAILED(pxQueue) Вызывается из API-функций xQueueSend(), xQueueSendToFront(), xQueueSendToBack() или из любой API-функции, которая возвращает семафор, если операция записи в очередь не была выполнена, так как очередь оставалась заполненной на протяжении указанного времени блокировки. Параметр: дескриптор очереди или семафора
traceQUEUE_RECEIVE(pxQueue) Вызывается из API-функции xQueueReceive() или из любой API-функции, которая захватывает семафор, если операция чтения из очереди выполнена успешно. Параметр: дескриптор очереди или семафора
traceQUEUE_RECEIVE_FAILED(pxQueue) Вызывается из API-функции xQueueReceive() или из любой API-функции, которая захватывает семафор, если операция чтения из очереди не была выполнена, так как очередь оставалась пустой на протяжении указанного времени блокировки. Параметр: дескриптор очереди или семафора
traceQUEUE_PEEK(pxQueue) Вызывается из API-функции xQueuePeek(). Параметр: дескриптор очереди
traceQUEUE_SEND_FROM_ISR(pxQueue) Вызывается из API-функции xQueueSendFromISR(), если запись в очередь прошла успешно. Параметр: дескриптор очереди
traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue) Вызывается из API-функции xQueueSendFromISR(), если запись в очередь не произошла, так как очередь заполнена. Параметр: дескриптор очереди
traceQUEUE_RECEIVE_FROM_ISR(pxQueue) Вызывается из API-функции xQueueReceiveFromISR(), если чтение из очереди прошло успешно. Параметр: дескриптор очереди
traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue) Вызывается из API-функции xQueueReceiveFromISR(), если чтение из очереди не произошло, так как очередь пуста. Параметр: дескриптор очереди
traceQUEUE_DELETE(pxQueue) Вызывается из API-функции vQueueDelete(). Параметр: дескриптор удаляемой очереди
traceTASK_CREATE(pxTask) Вызывается из API-функции xTaskCreate(), если задача успешно создана. Параметр: дескриптор создаваемой задачи
traceTASK_CREATE_FAILED(pxNewTCB) Вызывается из API-функции xTaskCreate(), если задача не создана из-за отсутствия памяти. Параметр: указатель на блок управления задачей, для размещения которого не хватило памяти
traceTASK_DELETE(pxTask) Вызывается из API-функции vTaskDelete(). Параметр: дескриптор удаляемой задачи
traceTASK_DELAY_UNTIL() Вызывается из API-функции vTaskDelayUntil()
traceTASK_DELAY() Вызывается из API-функции vTaskDelay()
traceTASK_PRIORITY_SET(pxTask,uxNewPriority) Вызывается из API-функции vTaskPrioritySet(). Параметры: 1) Дескриптор задачи. 2) Новое значение приоритета
traceTASK_SUSPEND(pxTask) Вызывается из API-функции vTaskSuspend(). Параметр: дескриптор приостанавливаемой задачи
traceTASK_RESUME(pxTask) Вызывается из API-функции vTaskResume(). Параметр: дескриптор задачи
traceTASK_RESUME_FROM_ISR(pxTask) Вызывается из API-функции xTaskResumeFromISR(). Параметр: дескриптор задачи

Переопределения макросов необходимо сделать в программе до включения заголо¬вочного файла FreeRTOS.h. Наиболее про¬стой путь добиться этого — разместить пере¬определения макросов трассировки в конце файла FreeRTOSConfig.h. Можно также соз¬дать отдельный заголовочный файл с трасси¬ровочными макросами и включить его в конец файла FreeRTOSConfig.h.
Трассировочный макрос может иметь параметр, который определяет, какая задача, очередь, семафор или мьютекс связаны с со¬бытием, в результате которого был вызван данный макрос. Полный список макросов трассировки приведен в таблице.
Рассмотрим пример практического применения макросов трассировки. В качестве аппаратной платформы будем использовать микроконтроллер AVR ATmega128L, установленный на мезонинный модуль WIZ200WEB фирмы WIZnet. Установка и настройка FreeRTOS для этой платформы рассматривались ранее [1, № 3], поэтому приводить здесь их не будем.

AVR ATmega128L является основой кон­троллера бесколлекторного двигателя посто­янного тока.

Отлаживаемое приложение — непосред­ственно программа управления контроллером, выполняющаяся под управлением FreeRTOS.

Информация о положении вала двигателя поступает от трехфазного датчика Холла (эн­кодера), встроенного в двигатель. Каждая фаза энкодера подключена к выводу ATmega128L так, что перепад логического уровня на каж­дой фазе вызывает внешнее прерывание ми­кроконтроллера.

Программа управления построена так, что обработчик каждого внешнего прерывания от энкодера записывает приращение угла по­ворота вала в очередь xDeltaPositionQueue длиной в 10 элементов (рис. 4). Задача Servo, занимающаяся непосредственно управлени­ем двигателем, принимает приращения угла из очереди, складывает их и формирует, та­ким образом, текущий угол поворота вала двигателя.

Задача трассировки: выяснить, достаточ­на ли длина очереди в 10 элементов для того, чтобы при максимальной скорости вращения двигателя (когда внешние прерывания от эн­кодера происходят с максимальной частотой) все приращения угла от каждого прерывания были записаны в очередь и в дальнейшем об­работаны задачей Servo.

Для этого необходимо отследить собы­тие неудавшейся попытки записи в оче­редь из тела обработчика прерывания. Трассировочный макрос traceQUEUE_SEND_ FROM_ISR_FAILED(pxQueue) вызывается именно в таком случае. Поэтому переопреде­лим его, добавив следующие строки в конец файла FreeRTOSConfig.h:

#define traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue) \

/* Если запись не удалась в очередь xDeltaPositionQueue. */ \

if (pxQueue == xDeltaPositionQueue) { \

/* Инвертировать вывод общего назначения PG3. */ \

DDRG |= (1 << DDG3); \

PORTG ^= (1 << PG3); \

}

В случае неудавшейся записи в очередь xDeltaPositionQueue будет инвертирован свободный вывод МК, состояние которого легко отследить с помощью осциллографа или даже по миганию светодиода. (К выводу PG3 подключен светодиод, расположенный прямо на модуле WIZ200WEB.)

Следует отметить, что в приведенном при­мере производится операция чтение/модифи­кация/запись регистров DDRG и PORTG, что является одним из случаев совместного до­ступа к ресурсу, который требует применения одного из механизмов взаимного исключе­ния [1, № 8]. Однако конкретно в этом слу­чае макрос traceQUEUE_SEND_FROM_ISR_ FAILED() вызывается из обработчика преры­вания, а особенность архитектуры AVR в том, что прерывания по умолчанию не являются вложенными. Поэтому переключение контек­ста не может произойти во время выполнения этого макроса трассировки. Следовательно, в применении каких-либо механизмов взаим­ного исключения нет необходимости.

Выполним сборку проекта, когда длина очереди задана 10 элементов, и запустим по­лученную отладочную версию программы в МК. Состояние вывода PG3 будем отслежи­вать осциллографом. Можно наблюдать, что логический уровень на выводе PG3 не меня­ется (рис. 5).

Следовательно, длины очереди xDeltaPositionQueue в 10 элементов достаточно для фиксации всех быстро следующих друг за другом событий (сигналов датчика Холла). Если же выполнить сборку проекта, установив длину очереди в 3 элемента, то можно видеть (рис. 6), что логический уровень на выводе PG3 теперь инвертируется. Следовательно, длины очереди в 3 элемента оказалось недостаточно.

Получение статистики выполнения задач

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

API-функция vTaskGetRunTimeStats() слу­жит для получения статистики в текстовой табличной форме в виде, представленном на рис. 7.

В данном случае для просмотра статисти­ки используется интернет-браузер Google Chrome. Компьютер подключен по Ethernet­ протоколу к целевому устройству, на кото­ром выполняется встроенный веб-сервер. Выполняющаяся на МК программа вызы­вает API-функцию vTaskGetRunTimeStats(), а результат ее работы представляет в виде интернет-страницы. Вывод статистики та­ким способом реализован в демонстрацион­ных проектах для микроконтроллеров NXP LPC17хх Cortex M3 и TI Stellaris LM3Sхххх из дистрибутива FreeRTOS.

В таблице статистики (рис. 7) каждой за­даче (задается по имени, поэтому разные эк­земпляры одной задачи отображаются под одним именем) ставится в соответствие два параметра:

  1. Абсолютное время выполнения (Abs time) — показывает, сколько единиц времени в сум­ме выполнялась та или иная задача до мо­мента вызова vTaskGetRunTimeStats(). Подробнее о единице времени сказано ниже.
  2. Относительное время выполнения (% Time) — показывает, какую часть от общего объема времени (от момента запуска плани­ровщика до вызова vTaskGetRunTimeStats()) составляет суммарное время выполнения той или иной задачи. Представлено в про­центах. Судить о загруженности процессора мож­но по суммарному времени выполнения за­дачи Бездействие (IDLE). Чем больше загру­жен процессор, тем меньше времени в про­центном отношении выполняется задача Бездействие.

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

Сразу стоит оговориться, что для отсчета суммарного времени выполнения задач по­требуется точный источник частоты, которая в 10–100 раз выше частоты следования си­стемных квантов. В качестве такого источни­ка целесообразно применять незадействован­ный таймер/счетчик микроконтроллера.

Настройки FreeRTOS для получения стати­стики выполнения задач:

  1. Макроопределение conf igGENERATE_ RUN_TIME_STATS в файле FreeRTOSConfig.h должно быть равно 1.
  2. В программе необходимо определить макрос portCONFIGURE_TIMER_FOR_ RUN_TIME_STATS(), который должен производить настройки таймера/счетчика для отсчета времени выполнения задач. Если макроопределение configGENERATE_ RUN_TIME_STATS задано равным 1, то данный макрос автоматически вызы­вается при запуске планировщика API­функцией vTaskStartScheduler().

Также необходимо определить макрос portGET_RUN_TIME_COUNTER_VALUE(), который должен возвращать текущее зна­чение таймера/счетчика, используемого для отсчета единиц времени работы задач. Если макроопределение configGENERATE_ RUN_TIME_STATS задано равным 1, то данный макрос автоматически вызыва­ется при переключении контекста. Остановимся подробнее на API-функции vTaskGetRunTimeStats(). Следует отметить, что она запрещает прерывания МК на время своего выполнения. Поэтому ее нужно ис­пользовать только для отладки; релиз про­граммы не должен содержать ее вызовов. Прототип vTaskGetRunTimeStats():

void vTaskGetRunTimeStats( portCHAR *pcWriteBuffer );

Единственный аргумент — указатель на буфер, в который будет помещена ин­формация в текстовом виде. Размер буфера не задан; выделяя для него память, следует придерживаться того, что для записи инфор­мации об одной строке (об одной задаче) по­требуется около 40 байт памяти.

Контроль переполнения стека

Как говорилось ранее [1, № 3], каждая за­дача во FreeRTOS имеет свой собственный стек. Память для размещения стека задачи выделяется на этапе создания задачи; объ­ем этой памяти задается как параметр API­функции xTaskCreate().

Рассмотрим подробнее, в каких случаях расходуется память стека задачи:
1. При возникновении прерывания МК. Состояние процессора (то есть набор его регистров) и адрес возврата сохраняются в стеке задачи, и управление передается обработчику прерывания. Если архитектура МК поддерживает вложенные прерывания, то каждый раз при возникновении нового прерывания, когда обработчик «старого» еще не завершился, происходит еще одно сохранение в стеке текущей задачи состояния процессора.
2. При вызове функции. Помимо текущего состояния процессора и адреса возврата, в стек помещаются аргументы вызываемой функции. Вызываемая функция в свою очередь тоже может вызывать функции, что приводит к соответствующей записи в стек.
3. При использовании локальных переменных. Все локальные переменные функции (а следовательно, и задачи, так как задача представлена в программе в виде функции языка Си) по умолчанию располагаются в памяти стека. Обработчик прерывания в программе на Си также представлен функцией, поэтому переменные, объявленные внутри обработчика прерывания, также размещаются в стеке.
Переполнение стека — это ситуация, когда в результате рассмотренных выше случаев выделенного объема памяти для стека оказывается недостаточно, и данные, которые должны были попасть в стек задачи, записываются за пределами отведенной для этого области. Распределение памяти в случае переполнения стека показано на рис. 8б. На рис. 8а в стек Задачи 2 были записаны какие-то данные, однако их объем оказался в пределах выделенной Задаче 2 области памяти для размещения стека. На рис. 8б в результате дальнейшего роста стека Задачи 2 размер стека превысил отведенный ему диапазон, из-за чего были повреждены данные, принадлежащие Задаче 1. Причем эффект от такого повреждения предсказать практически невозможно.
Таким образом, переполнение стека нарушает один из основных принципов работы многозадачной системы — принцип независимого выполнения нескольких задач на одном процессоре. Переполнение
стека — одна из наиболее частых причин краха программы не только во встраиваемых приложениях. Крайне сложно выявить возможность возникновения переполнения стека аналитически, поэтому на практике прибегают к экспериментальному определению размера стека, необходимого задаче, а также к механизмам обнаружения факта переполнения, что значительно облегчает поиск причины некорректной работы программы.
FreeRTOS предоставляет два метода обнаружения (и, возможно, коррекции последствий) переполнения стека. Для выбора одного из методов следует задать значение макроопределению configCHECK_FOR_STACK_OVERFLOW, равное 1 или 2, в файле FreeRTOSConfig. h. 0 означает отключенный механизм контроля переполнения стека. Важно, что контроль переполнения стека
возможен только на архитектурах, память которых не разбита на сегменты (например, порт для x86‑процессоров в реальном режиме не допускает использования механизмов контроля переполнения стека, так как x86‑процессоры имеют сегментированную модель памяти).
Кроме того, некоторые процессоры могут генерировать аппаратное исключение в ответ на переполнение стека до того, как оно будет обработано средствами FreeRTOS. Если включена опция контроля над переполнением стека, то в случае его возникновения автоматически вызывается функция обратного вызова vApplicationStackOverf lowHook(), которая должна содержать действия по индикации и, возможно, устранению последствий переполнения стека. Если включена опция контроля переполнения стека, то программа должна содержать определение функции vApplicationStackOverf lowHook(). Ее прототип:
void vApplicationStackOverflowHook( xTaskHandle *pxTask, signed portCHAR *pcTaskName );
В аргументы pxTask и pcTaskName автоматически помещаются, соответственно, указатель на дескриптор и имя задачи, стек которой переполнился. Однако следует помнить, что эффект от переполнения стека может быть разным, в том числе и таким, что аргументы функции vApplicationStackOverf lowHook() могут оказаться искаженными. В этом случае надежнее прочитать значение глобальной пе-
ременной pxCurrentTCB, которая определена в файле tasks.c и содержит указатель непосредственно на блок управления (на структуру tskTCB) той задачи, которая в данный момент выполняется.
Помните, что задействованный контроль переполнения стека приводит к дополнительному расходу времени процессора каждый раз при переключении с одной задачи на другую, поэтому использовать его следует только на этапе разработки и тестирования программы.
Метод контроля переполнения стека № 1

Потребление памяти стека с большой долей вероятности достигает своего максимального значения в тот момент, когда задача выходит из состояния выполнения,
так как стек задачи хранит ее контекст. В этот момент ядро может проверить регистр «Указатель стека» микроконтроллера на предмет, выходит ли его значение из заданного диапазона. Функция обратного вызова vApplicationStackOverf lowHook() вызывается, если значение указателя стека превышает объем стека, выделенный задаче при ее создании.
Это не гарантирует отслеживание 100% всех случаев переполнения стека. Например, данный метод не выявит переполнение стека, вызванное одновременным срабатыванием большого числа прерываний.
Для активации этого метода значение макроопределения configCHECK_FOR_STACK_OVERFLOW следует задать равным 1.

Метод контроля переполнения стека № 2

Ядро выполняет контроль переполнения стека по методу № 1, но дополнительно выполняет следующую проверку.

Когда задача создается, ее стек побайтно за­полняется известным значением (конкретно значением 0xA5). Когда задача покидает со­стояние выполнения, ядро проверяет верхние 16 байт памяти ее стека. Если хотя бы один байт из них оказался не равен изначально за­писанному известному значению (из-за ак­тивности задачи или прерывания), то делается вывод, что стек был переполнен, и вызывается функция vApplicationStackOverf lowHook().

Данный метод более эффективен, чем первый, но требует и больше времени на вы­полнение. Его особенность в том, что, если верхушка стека достигнет области послед­них 16 байт выделенной для него памяти, но не превысит выделенного объема, то ме­тод укажет на переполнение стека, которого в сущности не было.

Как и первый, этот метод не гарантирует фиксацию всех случаев переполнения стека. Для активации контроля по методу № 2 зна­чение макроопределения nfigCHECK_FOR_ STACK_OVERFLOW следует задать рав­ным 2.

Пример контроля переполнения стека

Рассмотрим практическое применение контроля переполнения стека на примере контроллера бесколлекторного двигателя по­стоянного тока, описанного выше. Цель — выяснить, наступает ли в программе ситуа­ция переполнения стека любой из задач, и, соответственно, подобрать такой объем вы­деляемого задаче стека, чтобы ситуация пере­полнения не наступала.

В программе создаются три задачи следую­щим образом:

/* Создание задач */

xTaskCreate(vAccelTask, ( signed char * ) "Accel", configMINIMAL_STACK_SIZE + 100, NULL, PRIORITY_ACCEL_TASK, NULL );

xTaskCreate(vServoTask, ( signed char * ) "Servo", configMINIMAL_STACK_SIZE + 100, NULL, PRIORITY_SERVO_TASK, NULL );

xTaskCreate(vProcessingTask, ( signed char * ) "Proc", configMINIMAL_STACK_SIZE + 100, NULL, PRIORITY_PROCESSING_TASK, NULL );

Размер минимального достаточного раз­мера стека conf igMINIMAL_STACK_SIZE взят равным 85 байт, так, как рекомендуется в демонстрационном проекте из дистрибути­ва FreeRTOS.

Включим контроль стека по методу № 2, добавив в файл FreeRTOSConfig.h строку:

#define configCHECK_FOR_STACK_OVERFLOW 2

Определим функцию обратного вызова, которая автоматически вызывается при пере­полнении стека:

void vApplicationStackOverf lowHook(xTaskHandle *pxTask, signed portCHAR *pcTaskName ) {

/* Инвертировать вывод общего назначения PG3. */ DDRG |= (1 << DDG3); PORTG ^= (1 << PG3);

}

Таким образом, переполнение стека любой задачи в системе будет приводить к инверти­рованию вывода МК, к которому подключен светодиод.

Если скомпилировать программу, загрузить ее в МК и подать на него питание, то по мига­нию светодиода можно видеть, что перепол­нение стека происходит периодически.

Зададим размер стека для каждой за­дачи на 100 байт больше, то есть равным configMINIMAL_STACK_SIZE + 200. После подачи питания можно видеть, что проблема не исчезла: светодиод продолжает мигать.

Зададим значение макроопределения configMINIMAL_STACK_SIZE равным 90 байт вместо 85. Прогон программы показывает, что проблема переполнения стека исчезла.

Такое поведение объясняется тем, что в программе автоматически создается зада­ча Бездействие, причем размер стека для нее задается равным configMINIMAL_STACK_ SIZE. Очевидно, что этого размера оказа­лось недостаточно, особенно в моменты воз­никновения прерываний микроконтроллера. Поэтому возникновение прерываний во вре­мя выполнения задачи Бездействие приводи­ло к переполнению стека.

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

Тем не менее если активировать контроль по методу № 1:

#define configCHECK_FOR_STACK_OVERFLOW 1

то прогон программы показывает отсут­ствие переполнения, даже при значении configMINIMAL_STACK_SIZE, равном 85. Это может говорить как о том, что при контроле по методу № 2 переполнения стека не было, а была лишь запись данных в последние 16 байт области стека, так и о том, что метод № 1 не выявил факт переполнения.

Необходимо оставить некоторый «запас прочности» по минимальному объему сте­ка, поэтому в рабочей программе он равен 90 байт.

Отладка программы с помощью StateViewer

Компания WITTENSTEIN, занимаю­щая продвижением коммерческих версий FreeRTOS — OpenRTOS и SafeRTOS, предла­гает утилиту StateViewer, которая встраивает­ся в среду разработки Eclipse, а также в среды разработки фирм IAR и Keil.

Утилита позволяет получить в режиме ре­ального времени отладочную информацию обо всех задачах и всех очередях (и объек­тах, на них основывающихся, — семафорах и мьютексах) при отладке встраиваемого приложения в вышеперечисленных средах разработки. Кроме OpenRTOS и SafeRTOS, эта утилита работает и с FreeRTOS.

Утилита StateViewer бесплатна, ее можно загрузить с [4], указав свой ящик электрон­ной почты.

Установка утилиты подробно описана в документации на нее, которая поставляется вместе с самой утилитой, поэтому останав­ливаться на этом не будем.

Рассмотрим пример работы с утилитой StateViewer на примере среды разработки Eclipse. Для того чтобы получить отладочную информацию, достаточно перейти в режим отладки приложения и вызвать на экран два окна: Task Table (рис. 9) и Queue Table (рис. 10), представляющие информацию, соответствен­но, о задачах и очередях в программе. Сделать это можно выбрав пункт меню Window Show View Other… OpenRTOS Viewer. Далее утилита сама соберет информацию о су­ществующих в программе задачах и очередях. Информация в окнах Task Table и Queue Table  обновляется только после приостановки выполнения программы (на­пример, с помощью точки останова).

Что касается самой FreeRTOS, то для того, чтобы очереди были представлены именами (как показано на рис. 10), необходимо свя¬зать очередь, заданную своим дескриптором, с ее именем в виде текстовой строки. В терминах FreeRTOS это называется добавле¬нием очереди в реестр. Для этого предназначена API-функция vQueueAddToRegistry(). Ее прототип:
void vQueueAddToRegistry(xQueueHandle xQueue, signed portCHAR *pcQueueName );
Аргументы API-функции vQueueAddToRegistry():
1. xQueue — дескриптор очереди (семафора, мьютекса), которую необходимо добавить в реестр.
2. pcQueueName — имя очереди (семафора, мьютекса), заданное в виде нуль-терминальной строки. Именно эта строка будет ото¬бражаться в списке очередей в окне Queue Table. Стоит отметить, что API-функция vQueueAddToRegistry() не имеет другого предназначения, кроме как для целей отладки. Причем добавлять в реестр следует только те очереди, поведение которых необходимо выяснить в ходе отладки. Макроопределение configQUEUE_ REGISTRY_SIZE в файле FreeRTOSConfig.h задает максимальное количество очередей, которые могут быть добавлены в реестр.

Пример добавления очереди в реестр:

void vAFunction( void ) { xQueueHandle xCharQueue;
/* Создать очередь для хранения 10 символов. */
xCharQueue = xQueueCreate( 10, sizeof( portCHAR ) );
/* Занести ее в реестр, таким образом мы сможем наблюдать ее в StateViewer.
под именем, соответствующим имени очереди в программе. */
vQueueAddToRegistry(xCharQueue, "xCharQueue" );
}

Если очередь добавлена в реестр и программа предусматривает ее удаление, то прежде чем удалять, очередь необходимо исключить из реестра. Для этого предназначена API-функция vQueueUnregisterQueue(). Ее прототип:
void vQueueUnregisterQueue( xQueueHandle xQueue );
Единственный аргумент xQueue — дескриптор очереди, которую необходимо исключить из реестра. Таким образом, процедура удаления очереди, которая ранее была помещена в реестр, выглядит так:
vQueueUnregisterQueue( xQueue ); vQueueDelete( xQueue );
Список задач Task Table Представляет собой список всех задач, созданных на данный момент в программе. Каждой задаче соответствует одна строка таблицы (рис. 9), причем зеленым цветом выделена выполняющаяся в данный момент задача. Красным цветом помечены те задачи, которые изменили свое состояние с момента последней приостановки выполнения программы. Каждому параметру задачи соответствует один столбец таблицы. Список параметров:
1. Task Name — имя задачи, назначенное ей в момент создания (аргумент API-функции xTaskCreate()).
2. Task Number — уникальный номер задачи, который автоматически был назначен ей ядром.
3. Priority/actual — приоритет задачи в данный момент. Работа меха¬низма наследования приоритетов может стать причиной временного повышения приоритета задачи по сравнению с приоритетом, с каким задача была создана.
4. Priority/base — приоритет задачи, назначенный ей в момент создания или измененный с помощью вызова API-функции vTaskPrioritySet().
5. Start of Stack — адрес начала стека задачи. Стек заполняется на¬чиная именно с адреса начала стека.
6. Top of Stack — адрес вершины стека задачи в данный момент. Именно по этому адресу был сохранен контекст задачи, находящейся в блокированном или приостановленном состоянии.
7. State — текущее состояние задачи. Может принимать следующие значения:
Running — выполняется в данный момент.
Ready — готова к выполнению.
Blocked — блокирована.
Suspended — приостановлена.
• 8. Event Object — имя или адрес очереди (семафора, мьютекса), ожи¬дание операции с которой послужило причиной перехода задачи в блокированное состояние.
• 9. Min Free Stack — минимальный объем свободной памяти стека за все время отладки программы. Равен минимальной разнице между адресом конца стека, который определен в момент создания задачи, и адресом вершины стека, который «плавает» в зависимости от вызова функций, срабатывания прерываний и сохранения контекста задачи. Вычисление объема свободного стека занимает существенное время,поэтому можно отключить эту возможность, щелкнув правой кнопкой в любом месте окна Task Table и выбрав Toggle Stack Checking.

Список очередей Queue Table
Как и в случае списка задач, красный цвет обозначает произошедшие изменения в состоянии очереди с момента последней остановки программы (рис. 10). Для каждой очереди выводятся следующие поля:
1. Name — имя очереди, которое было присвоено ей в момент, когда она была помещена в реестр.
2. Address — адрес структуры управления очередью, он же — дескриптор очереди.
3. Max Length — размер очереди, то есть максимальное количество элементов, которые одновременно может хранить очередь. Определяется в момент создания очереди.
4. Item Size — размер одного элемента очереди в байтах.
5. Current Length — длина очереди, то есть количество элементов, которые в данный момент хранятся в очереди. Всегда меньше раз¬мера очереди.
6. # Waiting Tx — количество задач, которые заблокировались, ожидая, когда в очереди появится свободное место (когда появится возможность записи в очередь).
7. # Waiting Rx — количество задач, которые заблокировались, ожидая, когда в очередь будет записан хотя бы один элемент (когда появится возможность прочитать элемент из очереди).

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

Leave a Reply

You must be logged in to post a comment.