Программирование DSP Обзор VisualDSP++ Kernel RTOS (VDK) Fri, December 13 2024  

Поделиться

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

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


Обзор VisualDSP++ Kernel RTOS (VDK) Печать
Добавил(а) microsin   

В этой статье приведен перевод основных частей даташита [1], посвященного среде выполнения реального времени (RTOS) на основе VisualDSP++ Kernel (сокращенно VDK) от компании Analog Devices. Здесь рассмотрены основные термины и понятия, используемые VDK (потоки, планировщик, очередь готовности, приоритеты и т. п.).

В среду VisualDSP++ встроена возможность использования в проектах ядра для системы реального времени (RTOS). Это так называемое ядро Kernel VDK. VDK поддерживает процессоры:

• Blackfin® (ADSP-BFxxx)
• SHARC® (ADSP-21xxx)
• TigerSHARC® (ADSP-TSxxx)

Общие принципы организации RTOS на основе VDK совпадают с принципами других операционных систем реального времени, таких как FreeRTOS [3] или scmRTOS [4]. Однако детали реализации и API имеют отличия.

[VDK и функция main()]

В отличие от других операционных систем реального времени, VDK определяет свою собственную функцию main(), предназначенную для инициализации и запуска VDK. Таким образом, программист не должен писать функцию main, за него это делает система разработки VisualDSP++ в процессе создания приложения на основе VDK (точнее, используется готовая библиотечная функция main из VDK).

Функция main() в библиотеках VDK определена так:

int main(void) {
   VDK::Initialize();
   VDK::Run();
}

Прототипы используемых здесь функций Initialize() и Run() выглядят следующим образом. На языке C++:

void VDK::Initialize(void);
void VDK::Run(void);

На языке C:

void VDK_Initialize(void);
void VDK_Run(void);

Вы можете заменить определенную внутри VDK функцию main() на Вашу собственную реализацию, в которой должны присутствовать вызовы Initialize() и Run().

Примечание: до вызова VDK::Initialize() может использоваться единственный API-вызов, это ReplaceHistorySubroutine(). Также не может быть произведено никаких переключений контекстов между потоками, пока не будет вызвана функция VDK::Run().

[Потоки приложения. Типы потоков]

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

В системе VDK поток является инкапсуляцией некоторого алгоритма и связанных с ним данных. Когда создается новый проект, используйте эту предоставляемую ядром идею потока, и с её помощью уменьшайте сложность Вашей системы. Поскольку множество алгоритмов можно рассматривать как процесс, составленный из блоков "подалгоритмов", то приложение можно разделить на малые функциональные блоки, которые можно закодировать и протестировать по отдельности. Эти сборочные блоки становятся повторно используемыми компонентами в более надежных и масштабируемых системах.

Определение поведения потоков VDK дает создание типов потоков. Типы это шаблоны, которые определяют поведение и данные, связанные со всеми потоками такого типа. Наподобие типов в языке C или типов и классов C++, типы потока не используются напрямую, пока не будет создан экземпляр (инстанциация) типа потока. Можно создать множество экземпляров потоков одинакового типа, однако для каждого типа потока будет только одна слинкованная копия исполняемого кода. Каждый поток имеет свой собственный набор переменных, определенных для потока этого типа, свой собственный стек, и свой собственный C-контекст выполнения в реальном времени.

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

Типы потоков. Вы не создаете напрямую потоки; вместо этого Вы определяете типы потоков. Работающий поток является созданным экземпляром объекта типа потока, и это делается точно так же, как создается любой другой пользовательский объект на языке C++.

Вы можете создать несколько инстанциаций объектов одного и того же типа. Каждая инстанциация потока будет иметь собственный стек, состояние, приоритет, и другие локальные переменные. Вы можете отличать друг от друга отдельные инстанциации потоков одинакового типа (дополнительную информацию см. в разделе "Параметризация потоков"). Каждый поток может быть индивидуально идентифицирован по своему идентификатору ThreadID, и для обращения к нему как к потоку в вызовах kernel API может использоваться хендл потока (handle). Поток может получить доступ к своему собственному идентификатору ThreadID путем вызова GetThreadID(). ThreadID является достоверным в течение всего времени жизни потока - пока поток не будет уничтожен. После уничтожения потока его ThreadID становится недопустимым.

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

Какой функционал требуется от потока. Каждый тип потока требует 5 отдельных декларируемых и реализованных функций. Первоначальные "нулевые" реализации всех этих 5 функций предоставлены в шаблонах, на основе которых среда разработки VisualDSP++ генерирует код модуля для типа потока. Функция потока Run является точкой входа в поток. Для многих типов потока функции тела потока (Run) и обработки ошибки (ErrorHandler) являются только теми местами, которые пользователь должен модифицировать и добавить туда свой код. Другие функции выделяют и освобождают системные ресурсы в соответствующие моменты времени создания и уничтожения потока.

Для функционирования приложения на основе Kernel VDK предусмотрено создание два типа потоков: потоки приложения и потоки загрузки. В проекте VDK на закладке Kernel браузера проектов они распределены по двум разделам дерева настроек ядра Kernel -> Threads: Thread Types (здесь определяют потоки приложения) и Boot Threads (здесь определяют потоки загрузки). Для функционирования приложения необходимы как минимум один поток приложения и один поток загрузки.

Поток приложения. Динамически создаваемые потоки это так называемые потоки приложения.

[Стандартные функции потока]

Весь основной функционал алгоритма потока содержится в одной функции. Эта функция в C++ носит имя Run(), и в C/ассемблере RunFunction(). Функция Run() является грубым эквивалентом функции main() программы на языке C. Когда завершается работа функции Run, поток перемещается в очередь ожидания освобождения его ресурсов. Если функция Run никогда не завершается, то поток остается работать, пока не будет явно уничтожен.

int mainthread::ErrorHandler()

В этой функции пишется код обработки ошибки потока. По умолчанию сюда добавлен вызов функции ErrorHandler, которая запускает панику ядра (kernel panic) и остановку работы системы. Функция обработки ошибки потока будет вызвана ядром, когда самим потоком были провоцированы ошибки, связанные с вызовами API ядра. Функция ошибки передает описание ошибки в виде перечисления (см. определение SystemError). Она также может передавать дополнительную информацию, определение которой полностью зависит от идентификатора перечисления ошибки. Обработка по умолчанию для функции ошибки VDK подразумевает вызов функции KernelPanic. Подробнее про обработку ошибок VDK см. раздел "Error Handling Facilities".

mainthread::mainthread(VDK::Thread::ThreadCreationBlock &tcb) : VDK::Thread(tcb)

Это конструктор для создания объекта потока. Его код не работает в контексте потока, и вся традиционная инициализация алгоритма потока должна быть сделана в функции Run до входа в её бесконечный цикл.

Функция создания потока выглядит так же, как обычный конструктор C++. Функция предоставляет абстракцию, используемую функциями kernel API CreateThread() и CreateThreadEx(), чтобы позволить динамическое создание потока. Функция конструктора будет первой вызываемой функцией в процессе создания потока; она также отвечает за вызов конструктора потока InitFunction(). Так же, как и конструктор C++, функция создания потока вызывается в контексте потока, который порождает новый поток вызовом CreateThread() или CreateThreadEx(). Создаваемый поток не получит контекст выполнения, пока не будет завершены функции, которые его создают.

Функция, создающая поток, вызывает конструктор для потока, и гарантирует, что все выделения ресурсов для этого типа потока будут корректно выполнены. Если какое-то выделение ресурсов потерпело неудачу, то функция создания удаляет частично созданную инстанциацию потока и возвратит нулевой указатель. Если поток был успешно сконструирован, функция создания потока вернет указатель на созданный поток. Функция создания не должна вызывать DispatchThreadError(), потому что функции обработки ошибки CreateThread() и CreateThreadEx() будут сообщать об ошибке вызывающему создание потоку, когда функция создания потока вернет нулевой указатель.

Функция создания потока полностью представлена в шаблонах исходного кода C++. Для потоков, написанных на языках C или ассемблера, функция создания появляется только в заголовочном файле потока. Если поток выделяет данные в InitFunction(), то вы должны модифицировать функцию создания в заголовке потока, чтобы проверить, что выделения прошли успешно, и уничтожить поток в противном случае.

Поток определенного типа может быть создан во время загрузки приложения (boot time) путем указания потока загрузки имеющегося типа в среде разработки. Дополнительно, если количество потоков в системе известно заранее во время компиляции, то все потоки могут быть потоками загрузки (boot thread).

InitFunction/Constructor. InitFunction() (на языках C/ассемблера) и конструктор (на языке C++) предоставляют место в потоке для выделения системных ресурсов при динамическом создании потока. Поток использует malloc (или new), когда выделяет локальные переменные потока. Есть ограничения для вызовов VDK API, которые можно выполнить из конструктора потока (или из InitFunction()), потому что API вызывается во время инициализации VDK (для boot-потоков), или из различных контекстов потока (для динамически создаваемых потоков). См. таблицу 5-24 для уровней допустимости API (validity levels).

mainthread::~mainthread()

Это деструктор для удаления объекта потока. Его код не работает в контексте потока, вся традиционные завершающие действия алгоритма потока (вызовы функций VDK API) должны быть сделаны в функции Run после выхода из её бесконечного цикла.

Деструктор вызывается системой, когда поток уничтожается. Поток может явно сделать это с вызовом DestroyThread(). Поток уничтожается также, если его функция Run достигла своего завершения (вышла за пределы своего бесконечного цикла while). Во всех случаях Вы отвечаете за освобождение памяти и других системных ресурсов, которые занимал поток. Любые области памяти, выделенные в конструкторе через malloc или new, должны быть освобождены соответствующим вызовом для освобождения или удаления в деструкторе.

Поток не обязательно будет уничтожен сразу, когда была вызвана функция DestroyThread(). DestroyThread() принимает параметр, который дает выбор приоритета, с которым дается выбор, когда должен быть вызван деструктор потока. Если второй параметр, inDestroyNow, равен FALSE, то поток помещается в очередь потоков, которые будут очищены потоком ожидания (Idle Thread), и деструктор будет вызван с приоритетом ниже, чем приоритет любого из потоков пользователя. У этой схемы есть много преимуществ, и она работает, в сущности, подобно сборщику мусора (garbage collector). В таком механизме нет предопределенного поведения, и нет гарантий, когда реально будут освобождены ресурсы для других потоков. Потоки, поставленные в очередь на очистку потоком ожидания (Idle Thread), также могут быть очищены вызовом API-функции FreeDestroyedThreads().

Если в аргументе inDestroyNow, переданном в DestroyThread(), задано значение TRUE, то деструктор будет вызван немедленно. Это гарантирует, что ресурсы будут освобождены при возврате из функции, но деструктор эффективно вызывается с приоритетом текущего работающего потока, даже если уничтожается поток с более низким приоритетом.

VDK::Thread* mainthread::Create(VDK::Thread::ThreadCreationBlock &tcb)

Еще один конструктор, пока не понял, для чего он нужен.

Поток загрузки (Boot Thread). Это поток, существование которого известно уже на этапе компиляции, т. е. он запускается с момент загрузки приложения VDK.

Код для реализации различных потоков может быть написан на языках C, C++ или ассемблера. Выбор языка прозрачен (т. е. не имеет значения) для ядра. Среда разработки VisualDSP генерирует хорошо документированный скелетный код для всех трех вариантов выбора.

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

Потоки на языке C++. У этих потоков самый простой код шаблона из всех трех поддерживаемых языков. Потоки пользователя это наследуемые классы от базового класса VDK::Thread. Потоки C++ незначительно отличаются именами функций и включают функцию Create(), а также конструктор.

Так как типы потоков пользователя являются унаследованными классами от абстрактного базового класса VDK::Thread, то могут быть добавлены переменные - члены класса в пользовательские классы потока (в заголовке определения класса), точно так же, как это делается с любым другим классом C++. Обычные правила языка C++, управляющие объектами, также относятся и к потокам, так что потоки могут использовать свои члены с различным типом разграничения доступа к ним (public, private и static). Все переменные члены потока считаются специфичными для этого потока (или специфичными, отдельными для каждой инстанциации потока).

Дополнительно вызовы VDK API на языке C++ отличаются от вызовов на языках C и ассемблера. Функции VDK API лежат в пространстве имен (namespace) VDK. Например, вызов CreateThread() на C++ должен осуществляться как VDK::CreateThread(). Не представляйте полное пространство имен VDK в потоках C++ с помощью ключевого слова using.

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

В программировании C и ассемблера доступ к локальному состоянию потока осуществляется через хендл (handle, указатель на указатель), который передается как аргумент в каждую из четырех функций потока пользователя. Когда требуется больше одного слова состояния, выделяется блок памяти с помощью malloc() в функции типа потока InitFunction(), и хендл устанавливается для указания на новую структуру в этой памяти.

Каждый экземпляр типа потока выделяет для себя уникальный блок памяти, и когда выполнятся поток такого типа, то хендл ссылается на корректную область памяти. Обратите внимание, что в дополнение к доступной переменной в аргументе для всех функций типа потока, хендл может быть также получен в любой момент для текущего работающего потока с использованием API-вызова GetThreadHandle(). Реализации InitFunction() и DestroyFunction() для потока не должны вызывать GetThreadHandle(), но вместо этого должны использовать параметр, переданный в эти функции, поскольку они не выполняются в контексте инициализируемого или уничтожаемого потока.

[Параметризация потоков (Thread Parameterization)]

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

Размер стека. Каждый поток имеет свой собственный стек, который выделяется из кучи, и его размер задается пользователем на закладке VDK Kernel, или через CreateThreadEx() API. Полная модель времени выполнения C/C++ (run-time model), как это определено в соответствующем руководстве компилятора и библиотеки (VisualDSP++ 5.0 C/C++ Compiler and Library Manual), поддерживается на базе потока. В полной Вашей ответственности находятся следующие обстоятельства: хватит ли места в стеке для адресов возврата и передаваемых параметров всех вызовов функций для определенной модели работы кода в реальном времени, как будут использоваться структуры в коде пользователя, библиотеки и т. д. Переполнения стека не будут генерировать исключение, так что недостаточное выделенное место под стек потока может привести к трудно отлаживаемым (нестабильно повторяющихся) ошибкам в Вашей системе.

Примечание: в сборках с полной поддержкой отладки VDK (fully instrumented builds), когда поток удаляется либо путем достижения конца своей функции Run(), либо с помощью явного вызова DestroyThread(), в окно истории VDK (VDK History) будет записано событие типа kMaxStackUsed. Значение, записанное в этом событии, покажет использование стека потоком (сколько места в стеке поток использовал).

Приоритет. Каждый тип потока задает приоритет по умолчанию. Потоки могут менять свой собственный приоритет (или приоритеты других потоков) динамически использованием функций SetPriority() или ResetPriority(). Приоритеты изначально заданы ядром как перечисление (enum) типа Priority, где kPriority1 обладает самым высоким приоритетом (шедулером ему будет передано управление в первую очередь) в системе. Менее приоритетные потоки имеют соответственно приоритеты kPriority1, kPriority2 и т. д. (чем выше число, тем ниже приоритет). Количество доступных приоритетов ограничено размером слова процессора минус 2 (т. е. 30 уровней приоритетов 1..30), потому что приоритет 0 фиксировано отдан планировщику, а приоритет 31 отдан под поток ожидания Idle.

Параметризация потока. Чтобы отличить отдельные инстанции boot-потоков одного и того же типа, пользователи могут предоставить целое число со знаком. Значение вводится в поле Initializer на VDK-закладке Kernel, и передается конструктору потока (функции InitFunction потока, если поток определен на языке C) через поле user_data_ptr аргумента ThreadCreationBlock. Пример DiningPhilosophers, который поставляется вместе с инсталляцией VisualDSP++, показывает, как поле Initializer может использоваться в потоках C.

Чтобы отличить друг от друга экземпляры динамически создаваемых потоков одного и того же типа, пользователи могут вызвать API-функцию CreateThreadEx(), и предоставить поле типа void* через поле user_data_ptr field аргумента ThreadCreationBlock. После этого к указателю можно получить доступ в конструкторе потока (если поток написан на языке C, то это будет функция InitFunction).

Из-за того, что user_data_ptr это обычное поле и указатель, инициализатор передается по адресу. Инициализатор может быть распакован в конструкторе C++ так:

int initializer = *((int*)t.user_data_ptr);

Или вот так, если это делается в функции InitFunction() потока на языке C:

int initializer;
initializer = *((int *)pTCB->user_data_ptr);

Значение initializer обычно сохраняется в переменной, которая принадлежит самому потоку. В потоке C для этой цели может использоваться хендл потока (thread handle, который также передается в функцию InitFunction), но этого проще достигнуть в C++.

Имейте в виду, для динамически создаваемых потоков user_data_ptr может использоваться одинаково (например как указатель на уникальное целое число int), или как основной указатель на данные, специфичные для потока. Это требует использования CreateThreadEx() (которая принимает ThreadCreationBlock в качестве своего аргумента), чтобы создать потоки), а не CreateThread().

[Глобальные переменные]

Приложения VDK могут использовать глобальные переменные как обычные переменные (под переменной в данном случаем подразумеваются любые глобальные объекты языка C++, которые могут быть изменены/прочитаны со стороны разных потоков). На языке C или C++ переменная, определенная полностью только в одном исходном файле, декларируется как extern в других файлах, которые используют эту переменную. На языке ассемблера декларация .GLOBAL показывает переменную для использования снаружи по отношению к этому файлу исходного кода, и декларация .EXTERN разрешает ссылку на этот символ имени переменной во время процедуры линковки.

Тщательно планируйте, как Вы используете глобальные переменные в многопоточной системе. Ограничивайте всегда, где это только возможно, доступ к глобальной переменной со стороны одного потока (единичной инстанциации типа потока), чтобы избежать проблем реентерабельности. Должны использоваться критические (critical) и/или не обслуживаемые планировщиком (unscheduled) участки кода, чтобы защитить операции с глобальными сущностями, которые потенциально могут оставить систему в неопределенном состоянии, если не обеспечена атомарность доступа к глобальной переменной.

Из Википедии:

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

Реентерабельность тесно связана с безопасностью функции в многопоточной среде (thread-safety), тем не менее, это разные понятия (в практическом программировании под современные ОС термин «реентерабельный» на деле равносилен термину «thread-safe»). Обеспечение реентерабельности является ключевым моментом при программировании многозадачных систем, в частности, операционных систем.

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

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

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

[Как обрабатываются ошибки (Error Handling Facilities)]

VDK включает встроенные механизм обработки ошибок (error-handling), который позволяет Вам определить поведение в случае ошибки по отдельности для каждого типа потока. Каждый вызов функции в Части 5 "VDK API Reference" (справочник по VDK API) перечисляет возможные коды ошибки. Для получения полного списка ошибок см. "SystemError".

В основе механизма обработки ошибок VDK лежит предположение, что все вызовы функций обычно завершаются успешно, так что не требуется, чтобы код ошибки был возвращен и проверен вызывающим функцию кодом. Метод VDK отличается от обычного соглашения программирования языка C, в котором возвращаемое значение для каждого вызова функции должно быть проверено, чтобы удостовериться, что вызов завершился успешно, без ошибки. Хотя такая модель широко используется в обычном системном программировании, вызов функции системы реального времени для встраиваемых приложений (RTOS) очень редко (если вообще когда-либо) приводят к сбою. Когда возникал ошибка, система вызывает функцию ErrorFunction(), тело которой реализуется пользователем.

Вы можете вызвать GetLastThreadError(), чтобы получить последнюю произошедшую ошибку работающего потока. Вы также можете вызвать GetLastThreadErrorValue(), чтобы получить дополнительное описывающие значение, определение которого зависит от специфической ошибки. Дополнительную информацию см. в таблице 5-23. Функция потока ErrorFunction() должна проверить, должно ли значение, возвращенное GetLastThreadError(), быть обработано интеллигентно, и может выполнить для этого соответствующие операции. Любые ошибки, которые поток не может обработать, должны быть переданы функции обработки по умолчанию потока (default thread error function), которая вызовет KernelPanic. Для получения инструкций, как передавать ошибку в функцию ошибки, см. комментарии в сгенерированном коде потока.

[Потоки и взаимодействие с аппаратурой]

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

Фреймворк VDK Interrupt Service Routine framework поощряет Вас удалить специальную привязку к аппаратуре из алгоритмов, встроенных в потоки (см. рис. 1-2). Прерывания перенаправляют информацию в потоки через сигналы драйверам устройств, или напрямую через сигналы в потоки. Использование сигналов для подключения аппаратуры к алгоритмам позволяет ядру планировать выполнение потоков на базе возникновения асинхронных событий.

VDK Device Drivers Entry Points

Рис. 1-2. Точки входа в драйверы устройств.

Домены кода. VDK делит выполнение кода между двумя основными доменами, в каждом из которых применяется свой метод планирования. Это домен прерываний (Interrupt Domain) и домен потоков (Thread Domain). Основное отличие этих доменов в планировании выполнения - домен прерываний управляется аппаратно контроллером прерываний (на основе приоритетов контроллер запускает то или иное прерывание и обеспечивает вложенность прерываний, если это разрешено), а домен потоков управляется программным планировщиком VDK (sheduler), базируясь на приоритете потоков (планировщик переключает контекст потоков, запуская тот поток, у которого в данный момент самый высокий приоритет).

Среда реального времени выполнения кода VDK может рассматриваться как мост между двумя этими доменами: thread domain и interrupt domain. Interrupt domain обслуживает аппаратуру с минимальными знаниями об алгоритмах, и thread domain абстрагируется от подробностей реализации аппаратуры. Драйверы устройств и сигналы работают мостом между этим двумя доменами.

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

[Планировщик (scheduler)]

Роль планировщика - гарантировать, что потоку с самым высоким приоритетом предоставлена возможность запуститься как можно быстрее. Планировщик никогда не вызывается напрямую потоком, но вызывается всякий раз, когда происходит вызов kernel API - произведенный либо из потока, либо из обработчика прерывания (Interrupt Service Routine, ISR), что меняет наиболее приоритетный поток. Планировщик не вызывается во время выполнения кода из критичного (critical) или не обслуживаемого (unscheduled) кода, но может быть вызван немедленно при закрытии любого типа защищенной области кода.

VDK State Diagram

Рис. 1. Диаграмма состояний приложения VDK.

Очередь готовности (Ready Queue). Планировщик в своей работе полагается на внутреннюю структуру данных, известную как очередь готовности. Очередь содержит в себе ссылки на все потоки, которые не заблокированы или не находятся в режиме сна. Каждый из всех потоков в очереди готовности имеют ресурсы, необходимые для продолжения работы; они просто дожидаются своей порции процессорного времени. Исключение составляет выполняющийся в настоящее время поток, который все равно остается в очереди готовности во время своего выполнения.

Очередь готовности называется очередью потому, что она организована по принципу расстановки приоритетов как буфер первым-пришел-первым-вышел (First-In First-Out, FIFO). Таким образом, когда поток перемещается в очередь готовности, он добавляется как последняя запись с его приоритетом. Например, если в очереди готовности есть 4 потока с приоритетами kPriority3, kPriority5 и kPriority7, и делается готовым к с запуску дополнительный поток с приоритетом kPriority5 (см. рис. 3-1).

VDK Ready Queue fig3 1

Рис. 3-1. Ready Queue.

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

VDK всегда работает как ядро с вытеснением задач по приоритету (preemptive kernel). Однако Вы можете использовать в своих интересах несколько разных режимов работы планировщика, чтобы упростить или усложнить алгоритм переключения между задачами в своих приложениях.

Кооперативный алгоритм планировщика (Cooperative Scheduling). Несколько потоков могут быть созданы с одинаковым уровнем приоритета. По самой простой схеме работы планировщика все потоки в системе получают одинаковый приоритет, и каждый поток получает доступ к процессору, пока сам вручную не уступит процессорное время для других потоков. Такая обработка переключения между задачами называется кооперативная многозадачность (cooperative multithreading). Когда поток готов уступить выполнение для следующего потока с таким же приоритетом, то поток может сделать это вызовом функции Yield(), поместив тем самым текущий работающий поток в конец списка очереди готовности. Дополнительно любой системный вызов, который приведет к блокировке текущего выполняемого потока, приведет к тому же результату. Например, если поток приостановлен на ожидании сигнала (pends on a signal), то он в настоящее время не доступен для запуска, и следующий поток в очереди продолжит свое выполнение.

Циклический алгоритм планировщика (Round-Robin Scheduling). Шедулинг по принципу Round-robin, который также называется переключением задач с квантованием по времени, позволяет нескольким задачам с одинаковым приоритетом автоматически получить процессорное время на фиксировано выделенный отрезок времени. В VDK уровни приоритета могут быть назначены как режим round-robin во время сборки, и их периоды могут быть указаны в тиках системы. Потоки с таким приоритетом работают соответствующее время, которое измеряется в тиках системы (VDK Tick). Если поток вытеснен потоком с более высоким приоритетом на значительное количество времени, время не отнимается из промежутка времени работы потока (time slice). Когда завершится период round-robin, он переходит в конец списка потоков с их приоритетом в очереди готовности. Обратите внимание, что период round-robin может дрожать, когда потоки вытесняются по приоритетам.

Алгоритм планировщика с вытеснением (Preemptive Scheduling). Полная вытесняющая многозадачность (preemptive scheduling), в которой поток получает процессорное время, как только он помещен в очередь готовности, если у него более высокий приоритет, чем работающий в настоящий момент поток, предоставляет больше мощности и гибкости, чем чисто кооперативный или циклический принцип переключения задач.

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

Запрет работы планировщика. Иногда требуется запретить планировщик, когда происходит манипуляция с глобальными объектами (или переменными). Например, когда поток пытается изменить сразу состояние больше, чем одного сигнала, то поток может войти в специальный не обслуживаемый планировщиком регион кода (unscheduled region), чтобы гарантировать, что все обновления будут сделаны атомарно. Не обслуживаемые планировщиком регионы это секции кода, которые выполняются с гарантированным отсутствием вытеснения другими (более приоритетными) потоками. Обратите внимание, что для не обслуживаемого региона прерывания все еще обслуживаются, но при выходе из обработки прерывания управление будет возвращено в тот же самый домен потока. В необслуживаемый регион происходит вход через вызов PushUnscheduledRegion(). Чтобы выйти из необслуживаемого региона, поток вызывает PopUnscheduledRegion().

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

Подробнее про понятие атомарности см. Википедию.

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

void My_UnscheduledFunction()
{
   VDK_PushUnscheduledRegion();
   /* В как минимум одном необслуживаемом регионе, но эта
       функция может использоваться из любого количества
       не обслуживаемых или критических регионов */
   /* ... */
   VDK_PopUnscheduledRegion();
}
 
void MyOtherFunction()
{
   VDK_PushUnscheduledRegion();
   /* ... */
   /* Этот вызов добавляет или удаляет один необслуживаемый регион */
   My_UnscheduledFunction();
   /* Здесь восстанавливаются необслуживаемые регионы */
   /* ... */
   VDK_PopUnscheduledRegion();
}

Есть дополнительная функция для управления необслуживаемыми регионами - PopNestedUnscheduledRegions(). Эта функция полностью извлекает из стека все необслуживаемые регионы. Хотя VDK включает в себя PopNestedUnscheduledRegions(), приложения не часто должны использовать эту функцию, чтобы не нарушать балансировку регионов.

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

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

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

Если вызов ISR API влияет на состояние системы, то API запускает программное прерывание с самым низким приоритетом. Когда это прерывание планируется для запуска аппаратным диспетчером прерываний, прерывание спускается до подпрограммы и входит в планировщик. Если прерванный поток не находится в необслуживаемом регионе, и перешел в готовность поток с более высоким приоритетом, то планировщик переключит управление из прерванного потока, и запустит другой поток с более высоким приоритетом. Программное прерывание с самым низким приоритетом уважает любые необслуживаемые регионы в работающем потоке. Однако прерывания все еще могут обработать драйвера устройств, выставить семафоры и т. д. На выходе из необслуживаемого региона планировщик запустится снова, и готовый к запуску более приоритетный поток станет работающим потоком (см. рис. 3-2).

[Поток фонового ожидания (Idle Thread)]

Поток Idle является в VDK-приложении заранее определенным, он автоматически создается с ThreadID, установленным в 0, и у него будет задан самый низкий приоритет, ниже чем все приоритеты потоков пользователя. Таким образом, если в очереди готовности нет потоков пользователя, то будет запущен поток Idle. Единственная существенная работа, которую делает поток Idle, это освобождение ресурсов уничтоженных потоков. Другими словами, поток Idle обрабатывает уничтожение потоков, которые были переданы в функцию DestroyThread(), где в параметре inDestroyNow было задано FALSE. В зависимости от платформы можно настроить основные свойства потока Idle, такие как его размер стека и куча, откуда будут выделяться все его необходимые области памяти (включая стек потока Idle). Дополнительную информацию Вы можете получить из онлайн Help. Могут быть зависящие от процессора требования к отдельным свойствам потока Idle (также дополнительную информацию см. в приложении A "Замечания, касающиеся используемого процессора").

VDK Thread State Diagram fig3 2

Рис. 3-2. Диаграмма состояний потока.

Время, проведенное в потоках кроме потока Idle, показано в процентах за длительный промежуток времени на закладке (Target Load) окна истории состояний (State History) среды разработки VisualDSP++. См. раздел "VDK State History Window" руководства [1] и онлайн Help для получения дополнительной информации об окне истории состояний.

[Создание приложения VDK]

Создать проект VDK очень просто - процесс ничем принципиально не отличается от создания обычного проекта приложения VisualDSP.

Процесс по шагам:

[Создание проекта]

1. File -> New -> Project... -> в разделе Project \ Select Type выберите тип проекта Project types: VDK application. В поле Name: введите имя проекта (например MyVDKproject), и в поле ввода Directory: введите полный путь до каталога, где будет создан корневой каталог для проекта. После этого кликните Next.

VisualDSP create VDK project wizard step01

На запрос создания новой папки, если она не существует (The directory, C:\projects\MyVDKproject, doesn'n exist. Do you want to create it?) ответьте положительно.

2. На этом шаге (Select Processor) выберите тип процессора и кликните Next.

VisualDSP create VDK project wizard step02 select CPU

3. На этом шаге (VDK Application Settings) оставьте все по умолчанию, Executable (.dxe) и кликните Next.

VisualDSP create VDK project wizard step03 select DXE

4. На этом шаге (Add Startup Code/LDF) выберите добавить файл LDF и код запуска (Add an LDF and startup code) и кликните Next.

VisualDSP create VDK project wizard step04b add LDF

5. Если Вы используете внешнюю память SDRAM, то поставьте галочку Use external memory (SDRAM) и выберите её размер, после чего кликните Next.

VisualDSP create VDK project wizard step05 LDF SDRAM

6. На этом шаге предложат настроить размер системной кучи. Оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step06 LDF system heap

7. На этом шаге предложат настроить размер кучи для пользователя. Оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step07 LDF user heap

8. На этом шаге предложат настроить размер стека системы. Оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step08 LDF system stack

9. На этом шаге предложат настроить дополнительные опции для LDF. Оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step09 LDF adv options

10. На этом шаге предложат настроить кэширование и защиту памяти. Оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step10 startup cache and mem protection

11. На этом шаге предложат настроить тактовую частоту и настройки питания. Оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step11 startup clock and power

12. На шаге Run-time Initialization оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step12 startup run time init

13. На шаге предложат настроить систему профилирования компилятора, оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step13 startup profiling

14. На шаге предложат настроить дополнительные опции приложения VDK, оставьте все по умолчанию и кликните Next.

VisualDSP create VDK project wizard step14 startup adv options

15. Создание проекта VDK закончено, нажмите Finish.

VisualDSP create VDK project wizard step15 finish

В окне браузера проектов (View -> Project Window) появится новый проект MyVDKproject, и он станет текущим.

VisualDSP VDK Project tab

Рядом с закладкой Project появится новая закладка Kernel, где можно конфигурировать настройки текущего проекта VDK.

VisualDSP VDK Kernel tab

[Создание типа для потока приложения]

На первый взгляд проект VDK выглядит несколько непривычно. Например, в нем нет привычной C-функции main. В проекте создано несколько файлов исходного кода (VDK.cpp, VDK.h, MyVDKproject_basiccrt.s, MyVDKproject_heaptab.c), т. е. все аналогично обычному проекту, но на этом сходство заканчивается. Теперь Вы не имеете права модифицировать созданные системой файлы VDK.cpp, VDK.h, и для добавления функционала приложения должны добавить в проект VDK потоки (Threads). Без наличия потоков пользователя приложение VDK будет неработоспособным, т. е. не сможет выполнять никакие функции.

16. У нас есть созданный скелет приложения, который был создан на шагах 1..15 (см. врезку "Пример создания приложения VDK в среде VisualDSP++"). Теперь создадим тип для потока, который будет выполнять простейшее действие - мигать светодиодом.

При попытке скомпилировать только что созданный проект VDK среда VisualDSP выведет запрос: "There must be at least one Thread Type defined before the project can be built. Would yo like to define one now?" ("Для компиляции проекта нужно определить хотя бы один тип потока. Хотите определить один такой?"). Ответьте "Да". Откроется окно для редактирования типа потока. Введите имя потока (в качестве примера на скриншоте ниже указано имя потока mainthread), и имя для исходного файла потока и файла заголовка подставятся автоматически. Оставьте остальные опции без изменения и кликните OK.

VisualDSP VDK Thread Type create

В корневой папке проекта будут автоматически созданы файлы mainthread.cpp и mainthread.h, которые уже можно редактировать, но по определенным правилам - в соответствии с концепцией, задаваемой Kernel VDK RTOS. В модуле mainthread.cpp будут созданы системные функции потока:

void mainthread::Run() 

В этой функции определяется основной код для инициализации потока и бесконечный цикл для его алгоритма. Пока здесь вставлен пустой бесконечный цикл while(1){}.

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

Способ 1. Создание потока загрузки. В этом случае создание экземпляра потока и запуск потока на выполнение берет на себя библиотека VDK. Единственное, что Вам требуется для запуска потока - создать в папке Boot Threads новый поток, и указать для него нужный тип. После этого созданный поток будет запускаться автоматически, когда приложение VDK загрузится и запустится на выполнение. Запускаемый таким способом поток называется потоком загрузки (Boot Thread).

Способ 2. Создание потока приложения. Экземпляр потока создается и запускается во время работы приложения VDK, в этом случае поток порождается программно из тела другого потока. Для создания потока используются стандартные методы создания экземпляров класса C++ (либо вызов VDK API, если Вы пишете приложение VDK на языке C или на ассемблере).

Самый простой способ первый, рассмотрим именно его.

17. Создание Boot Thread. В проекте VDK должен быть создан хотя бы один поток загрузки, его создание среда VisualDSP также запросит при попытке компиляции проекта: "There must be at least one Boot Thread defined before the project can be built. Would yo like to define one now?" ("Для компиляции проекта нужно определить хотя бы один поток загрузки. Хотите определить один такой?"). Ответьте "Да". На закладке Kernel браузера проекта будет предложено редактирование имени потока загрузки. Введите произвольное имя потока (например BootThread).

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

Откройте файл mainthread.cpp и найдите в нем функцию Run. Добавьте в неё следующий код (добавленный код выделен жирным шрифтом):

void
mainthread::Run()
{
   // TODO - здесь напишите код основной инициализации:
   *pPORTEIO_FER |= PE6;   //настроить ножку порта как GPIO.
   *pPORTEIO_DIR |= PE6;   //настроить ножку порта как выход GPIO.
   *pPORTEIO_SET  = PE6;   //==1, погасить светодиод
   while (1)
   {
      // TODO - здесь напишите код основного тела приложения:
      // Для выхода используйте инструкцию "break".
      *pPORTEIO_TOGGLE = PE6;
      VDK::Sleep(500);
   }
   // TODO - в этом месте напишите код завершения потока.
   // Поток будет автоматически уничтожен, когда он выйдет за пределы
   // бесконечного цикла while (1) и из этой функции Run.
   // ...
}

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

[Конфигурирование приложения VDK]

19. Библиотека VDK предоставляет базовые средства для конфигурирования приложения. Они расположены на закладке Kernel -> раздел System.

Частота процессора задается параметром Clock Frequency (MHz). Частоту процессора можно также менять runtime вызовом API-функции SetClockFrequency(), а узнать вызовом GetClockFrequency().

Длительность тика. Элементарный квант времени, который отсчитывается в приложении VDK, называется тиком. Его длительность конфигурируется параметром Tick Period (ms). По умолчанию задается 0.1, что составляет 0.1 миллисекунд.

[Словарик]

ADI аббревиатура, обозначающая компанию Analog Devices.

SSL system services libraries, библиотеки системных служб VDK.

VDK VisualDSP Kernel, ядро (а точнее система библиотек), предназначенное для написания многопоточных приложений в среде разработки VisualDSP++.

[Ссылки]

1. VisualDSP++ 5.0 Kernel (VDK) User’s Guide site:analog.com.
2. Проектирование с использованием процессоров Analog Devices. Первый проект. site:kit-e.ru.
3. FreeRTOS: практическое применение, часть 1 (управление задачами).
4. scmRTOS для Blackfin.
5. Конфигурирование и отладка проектов VDK.

 

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


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

Top of Page