Специфика использования VDK для процессоров Blackfin Печать
Добавил(а) microsin   

Здесь переведена информация из приложения A "Processor-Specific Notes" даташита [1], касающаяся только процессоров Blackfin компании Analog Devices (в [1] также приведена соответствующая информация и для семейств процессоров SHARC и TigerSHARC). Все, что здесь написано, касается процессоров моделей ADSP-BF504, ADSP-BF504F ADSP-BF506F, ADSP-BF512, ADSP-BF514, ADSP-BF516, ADSP-BF518, ADSP-BF522, ADSP-BF523, ADSP-BF524, ADSP-BF525, ADSP-BF526, ADSP-BF527, ADSP-BF531, ADSP-BF532, ADSP-BF533, ADSP-BF534, ADSP-BF535, ADSP-BF536, ADSP-BF537, ADSP-BF538, ADSP-BF539, ADSP-BF542, ADSP-BF544, ADSP-BF548, ADSP-BF549, ADSP-BF561 и ADSP-BF592-A.

[Режимы пользователя и супервизора]

Архитектура процессоров Blackfin создает отличия в условиях выполнения кода режима пользователя (user mode) и режима супервизора (supervisor mode). Однако весь код приложения VDK работает в режиме супервизора, включая весь код пользователя, который он размещает в потоках.

Поскольку supervisor mode предоставляет дополнительные возможности по доступу к ресурсам по сравнению с user mode, то приложения, использующие VDK, не должны беспокоиться о том, в каком режиме работает процессор, и код приложения не должен запускать исключения (raise exceptions), чтобы получить доступ к защищенным ресурсам процессора. Однако это не означает, что нелегальный доступ не будет приводить к аппаратным ошибкам, вызывающим соответствующие исключения и панику ядра.

[Уровни выполнения кода: Thread, Kernel, Interrupt]

VDK резервирует уровень выполнения приоритета 15 в качестве уровня выполнения реального времени для большинства кода пользователя и кода VDK API, и резервирует уровень выполнения приоритета 14 для внутренних операций VDK. Далее в тексте эти уровни упоминаются соответственно как "thread level" (уровень потока) и "kernel level" (уровень ядра). Уровни приоритета выполнения 13..6 коллективно зарезервированы за "interrupt level" (уровень прерываний). Все эти уровни (15–6) выполняются в режиме супервизора (supervisor mode).

Примечание: под уровнями приоритета имеются в виду приоритеты IVG, подробнее см. [5].

Сам поток, и все функции, вызываемые из него, выполняются на thread level (уровень выполнения 15, execution level 15), включая код функций Run() (основное тело потока, где содержится его бесконечный цикл) и ErrorHandler() (обработчик системных ошибок). Соответственно весь код обработчиков прерывания (Interrupt Service Routine, сокращенно ISR) выполняется с более высоким приоритетом (чем меньше номер приоритета, тем выше приоритет), в соответствии с источником, вызвавшим прерывание и настройками приоритета. Функция для реализации драйверов устройств (device drivers), т. е. единственная точка входа в драйвер, может быть вызвана ядром на любом thread level (execution level 15) или на kernel level (execution level 14), в зависимости от того, для чего предназначен вызов.

Функционал "активации" драйвера устройства (kIO_Activate) это только тот код пользователя, который выполняется на kernel level. Весь другой код драйвера устройства выполняется на thread level. Вход на kernel level в этом случае инициируется через ISR вызовом VDK_ISR_ACTIVATE_DEVICE_() или C_ISR_ActivateDevice(), и поэтому является асинхронным по отношению к коду thread level, исключая использование критического региона. Таким образом, нужно позаботиться о синхронизации доступа к общим данным между уровнями выполнения кода thread level и kernel level, как и между уровнями thread level и interrupt level (и также между kernel level и interrupt level). Для всех этих целей могут использоваться критические регионы.

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

Необслуживаемый регион кода (unscheduled region). "Необслуживаемый" относится к планировщику: т. е. во время работы необслуживаемого региона работа планировщика запрещена, чем обеспечивается гарантия, что выполнение необслужваемого региона текущего потока не будет вытеснено другим потоком. Однако обработчик прерывания может прервать выполнение необслуживаемого региона кода. Таким образом, необслуживаемый регион может обеспечить атомарность доступа только когда общие данные используются между потоками, и обработчики прерывания эти данные не используют.

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

Критический регион кода (critical region). Критический регион кода - это такой участок кода, во время выполнения которого прерывания запрещены. Этим обеспечивается полная атомарность, т. е. критическим регионом может обеспечиваться защита общих данных как для потоков, так и для прерываний. Критическим регионом также считается обработчик прерывания, для которого не разрешена вложенность прерываний.

PushCriticalRegion() запрещает прерывания (вход в критический регион), и вызов PopCriticalRegion() заново разрешает прерывания (выход из критического региона).

Подробнее про защищенные регионы см. раздел "Protected Regions" документации VisualDSP++ 5.0 Kernel (VDK) User’s Guide [2].

Поскольку VDK полностью работает в supervisor mode, то системные регистры процессора, привязанные к адресному пространству процессора (system memory-mapped registers, сокращенно MMR) доступны в любом месте кода и в любое время. Это упрощает взамодействие с аппаратными устройствами процессора, однако пользователь полностью отвечает за гарантию правильности работы с периферийными и системными устройствами через MMR, операции синхронизации шины и ядра (ssync, csync) и обеспечению правильных интервалов времени операций.

[Исключения (exceptions)]

Исключения это просто прерывания, предназначенные для специальных системных целей. VDK резервирует исключение 0 (user exception 0, EXCPT 0) для внутреннего использования.

Среда разработки VisualDSP++ автоматически генерирует и добавляет исходный файл во все проекты VDK, созданные для процессоров Blackfin. Этот исходный файл содержит код шаблона кода пользователя для обработки исключения (user exception handler), и определяет точку входа для любых исключений служб или исключений ошибки, которые Вы хотите перехватить и обработать. Когда происходит исключение, VDK перехватывает его. Если это не исключение VDK, то VDK пытается определить, было ли это исключение связано с промахом CPLB, и в таком случае будет запущен обработчик cplb_hdr. Если VDK не может идентифицировать исключение как исключение VDK, либо как исключение CPLB miss, то тогда выполнится обработчик исключения пользователя (user exception handler).

CPLB Cacheability Protection Lookaside Buffer, специальные пары регистров для кэширования памяти данных и инструкций (16 пар для данных и 16 пар для инструкций). Позволяет организовать защиту областей памяти от несанкционированного доступа (обычно используется в многозадачной среде). По умолчанию (после сброса процессора) эта функция отключена.

{/spolier}

Обработчик исключения пользователя не делает возврат либо из UserExceptionHandler, либо из cplb_hdr, поэтому должны использоваться короткие переходы (short jump), чтобы избежать потенциального повреждения содержимого регистра P1. По этой причине LDF-файл должен установить секции L1_code и cplb_code в памяти близко друг к другу, чтобы избежать ошибок линковки.

[Макросы ассемблера для ISR API]

Синтаксис ассемблера Blackfin [6] требует использования отдельных макросов API, в зависимости от того, являются ли аргументы константами (непосредственными значениями инструкции immediate value, перечислениями) или регистрами данных (R0..R7). Аргументы для макросов ассемблера по умолчанию, описанные в Главе 5 "VDK API Reference", должны быть константами. Когда в качестве аргументов передаются регистры данных, к имени макроса добавляется суффикс "REG_":

VDK_ISR_POST_SEMAPHORE_REG_(semaphore_num_);
VDK_ISR_ACTIVATE_DEVICE_REG_(dev_num_);
VDK_ISR_SET_EVENTBIT_REG_(eventbit_num_);
VDK_ISR_CLEAR_EVENTBIT_REG_(eventbit_num_);
VDK_ISR_LOG_HISTORY_REG_(enum_, value_, threadID_);

Макросы ассемблера, определенные в разделе "Макросы ассемблера C/C++ ISR API" (без суффикса “REG_”), в качестве аргументов принимают только константы. Попытка передать имя регистра приведет к ошибке ассемблера.

[Прерывания]

Следующие аппаратные события (hardware events, или interrupts) зарезервированы по умолчанию для использования VDK вместе с процессорами Blackfin.

EVT_EVX – обработчик программных исключений (software exception handler). Код пользователя обрабатывает программные исключения путем модификации исходного файла, создаваемого средой VisualDSP++, который получает имя ExceptionHandler-имя_процессора.asm (например ExceptionHandler-BF533.asm).

EVT_IVTMR – прерывание, связанное с таймером, интегрированным в ядро процессора. Этот таймер генерирует прерывания для отсчета системных тиков, и таким образом обеспечиваются все службы VDK для отсчета реального времени (VDK timing services). Запрет этого таймера останавливает засыпание потоков на заданный интервал времени (функция Sleep), прекращается работа планировщика по карусельному алгоритму переключения между потоками (round-robin scheduling), перестают работать блокировки потоков с таймаутами (pending with timeout), периодические семафоры, и теряет смысл запись в лог истории VDK с привязкой по времени (VDK history logging, см. врезку ниже). Прерывание, назначенное для таймера, может быть изменено или установлено в none через редактирование свойств проекта VDK (на закладке Kernel).

При сборке проекта VDK у Вас есть опция добавить дополнительную отладочную информацию и исполняемый код путем выбора в свойствах проекта варианта Full Instrumentation (закладка Kernel, раздел дерева System -> Instrumentation Level). Инструментальная сборка отличается от не инструментальной тем, что в инструментальной сборке добавлен лишний код, собирающий статистику по выполняемым потокам (thread statistic logging). Эта дополнительная запись в лог немного отнимает ресурсы процессора на вызовы специального API, но помогает в отладке, чтобы можно было отследить активность компонентов системы.

[Окно VDK State History]

VDK записывает в лог определенные пользователем события и изменения состояния системы с использованием кольцевого буфера. Событие регистрируется в буфере истории (history buffer) с помощью вызова LogHistoryEvent(). Вызов LogHistoryEvent() записывает в лог 4 значения данных: ThreadID вызывавшего потока, тик в момент вызоваd, перечисление и значение, специфичное для этого перечисления. Перечисления меньше, чем 0, зарезервированы для использования внутри VDK. Для дополнительной информации по типу перечисления истории (history enumeration), см. определение HistoryEnum.

VDK State History window

Предоставляется API для получения почти всех данных, которые показываются в окне VDK Status, так что эта информация становится доступной в приложении VDK. Подробнее это API описано в главе 5, "VDK API Reference", также см. [2].

Используя лог истории, среда разработки VisualDSP++ графически отображает работающие потоки и изменения системного состояния в окне State History. Обратите внимание, что данные, отображаемые в этом окне, обновляются только в момент остановки программы отладчиком (halt). Подробнее окно State History, легенды Thread Status и Thread Event описаны в онлайн-Help.

Есть возможность либо заменить подпрограммы VDK history logging, либо добавить определяемую пользователем функцию в существующий механизм history logging (см. [2]).

[Окно Target Load]

Инструментальные сборки VDK позволяют разработчику анализировать загрузку процессора в течение прошедшего периода времени. Вычисленная нагрузка отображается графически в окне Target Load. Хотя это вычисление не точное, график помогает оценить уровень использования ресурсов процессора. Обратите внимание, что информация на графике обновляется в момент остановки приложения VDK в отладчике. Для более точного вычисления см. пример LoadMeasurement, который находится в каталоге установки VisualDSP++.

VDK Target Load window

График Target Load показывает процент времени, которое целевой процессор (target) проводит в вычислениях на потоке ожидания (Idle thread). Нагрузка 0% означает, что приложение VDK все свое время проводит в потоке ожидания. Нагрузка 100% означает, что код потока ожидания практически не выполняется. Данные нагрузки обрабатываются перемещающимся окном усреднения. Нагрузка в процентах вычисляется для каждого тика, и все тики усредняются. Для вычисления процентной нагрузки (Load) на каждом тике используется следующая формула:

Load = 100% * (1 - (#idle / (#threads + #idle)))

#idle количество времени, проведенное в потоке ожидания на момент этого тика
#threads количество времени, проведенное в потоках на момент этого тика

Дополнительную информацию про график Target Load см. в онлайн-Help.

Примечание: информация VDK в окнах State History и Target Load доступна только для сборки в конфигурации Debug, даже если для конфигурации Release на закладке Kernel выбрано Instrumentation Level -> Full Instrumentation.

[Окно VDK Status]

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

Для получения большинства информации из этого окна предоставляется API, благодаря чему можно получать эти данные статистики во время работы приложения, без остановки отладчика. Подробнее это API описано в главе 5 "VDK API Reference".

VDK Status window1 VDK Status window2

Дополнительную информацию про график VDK Status см. в онлайн-Help.

Примечание: информация VDK Status доступна как для сборки Debug, так и для сборки Release, если включена инструментальная поддержка (в свойствах проекта на закладке Kernel выбрано Instrumentation Level -> Full Instrumentation).

EVT_IVG14 – общее прерывание 14. Это прерывание зарезервировано для использования библиотеками VDK, и оно не может использоваться ни для каких других целей.

EVT_IVG15 – общее прерывание 15. Это прерывание зарезервировано для предоставления режима супервизора (supervisor mode) для выполнения кода пользователя и кода VDK, и оно не может быть использовано ни для каких других целей.

Процессоры Blackfin обозначают аппаратные события от 7 до 13 (EVT_IVG7..EVT_IVG13) как "общие прерывания" (general interrupts), и каждое из них привязывается к одному или большему количеству физических периферийных устройств. Среда разработки VisualDSP++ генерирует шаблоны исходного кода с точкой входа, по одной на уровень прерываний, вместо того, чтобы делать по одной точке входа на каждое периферийное устройство. Таким образом, в Вашей ответственности сделать диспетчеризацию прерываний, когда к одному уровню привязано больше одного прерывания от разных периферийных устройств. Используемая для этого техника зависит от приложения, однако обычно используется обработка прерываний по цепочке или таблица переходов. Когда используется обработка по цепочке, обработчик проверяет флаги, чтобы проверить, какое периферийное устройство вызвало прерывание, и в зависимости от этого запустить нужный код обработки. Если проверенный флаг не установлен, то значит прерывание было вызвано другим устройством, и управление передается на следующий обработчик в цепочке. Таблица переходов использует идентификатор или константу в качестве индекса таблицы с указателями, когда осуществляется поиск подходящего ISR для обработки прерывания.

Из-за природы немаскируемого прерывания (non-maskable interrupt, EVT_NMI), VDK ISR API не может использовать ISR-ы, чтобы обработать прерывание NMI.

Примечание: на процессорах Blackfin есть возможность напрямую манипулировать содержимым регистра IMASK, вместо того, чтобы быть ограниченным API-вызовами SetInterruptMaskBits() или ClearInterruptMaskBits(). Эта прямая манипуляция не разрешена в конструкторах потоков загрузки (boot thread, это такие потоки, которые автоматически стартуют при запуске приложения VDK) или в функциях Init() драйверов устройств. Рекомендуется, чтобы инициализация системных служб (System Services [7]) происходила в main() после вызова VDK_Initialize() на языке C (или VDK::Initialize() на языке C++), потому что многие вызовы API Системных Служб напрямую модифицируют IMASK. Подробнее см. раздел "VDK и функция main()".

[Таймер]

Элементарные кванты времени приложения VDK это тики (Ticks). Они отсчитываются по таймеру, реализованному на основе аппаратного счетчика ядра Blackfin, и этот счетчик засинхронизирован с основной частотой ядра (main core clock, CCLK). Однако этот таймер запрещается и останавливается, когда процессор Blackfin входит в режим пониженного энергопотребления (low power mode). Таким образом, все службы отсчета времени VDK (VDK timing services, такие как обработка сна потоков, отслеживание таймаутов, периодические семафоры) не работают, пока ядро находится в состоянии IDLE или low power mode. По умолчанию VDK не использует и не модифицирует таймеры общего назначения Blackfin.

На процессорах Blackfin Вы можете зарегистрировать прерывания с помощью библиотечной функции register_handler() или API-вызовом системных служб adi_int_CECHook(), и при этом не нужно декларировать прерывания на закладке Kernel свойств проекта VDK. Прерывания, зарегистрированные вне VDK, не должны использовать тот же уровень IVG, как и любое прерывание, определенное на закладке Kernel проекта VDK.

Внимание: чтобы гарантировать, что VDK полностью прошел инициализацию перед обслуживанием прерывания, которые использует сигналы VDK, не вызывайте register_handler() или adi_int_CECHook() перед запуском функции Run() потока загрузки (boot thread) с самым высоким приоритетом (или перед функцией VDK::Run(), если была заменена VDK-функция main()).

Важно: не используйте adi_int_CECHook для любых уровней прерываний, зарезервированных для VDK. Вызов adi_int_CECHook на уровнях прерываний 3, 14, или 15 в приложении VDK возвратит ошибку, если приложение использует библиотеки отладки Системных Служб (System Services Debug Libraries). Если приложение использует библиотеки релиза (Release System Services libraries), то прерывания VDK могут быть перезаписаны, и приложение может не работать так, как ожидалось изначально.

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

Поток ожидания VDK освобождает ресурсы системы, связанные с уничтожением потоков [9]. После этой операции поток ожидания бесконечно зацикливается, не вводя процессор в режим пониженного энергопотребления low power mode. Причина этого в том, что режимы сна и пониженного потребления тока останавливают таймер ядра, что останавливает службы, связанные с обработкой реального времени VDK.

[Память процессора Blackfin]

Файлы описания линкера VDK по умолчанию для процессоров Blackfin (VDK-BF531.ldf, VDK-BF532.ldf, и т. д.) помещают код, данные, и системную кучу (system heap) в области памяти по умолчанию. Эти установки по умолчанию можно поменять ручным редактированием файла .ldf, который используется в проекте. Подробнее о том, как это делается, см. руководство по линкеру и утилитам [4]. Альтернативная привязка подключается в файлах .ldf по умолчанию, которые используют часть или все пространство L1 SRAM в качестве кэша (подразумевается, что имеется в наличии внешняя память, обычно это SDRAM). Однако кэширование кода и данных по умолчанию не разрешено. Подробнее о том, как разрешить и конфигурировать кэширование, см. руководство по компилятору и библиотекам для процессоров Blackfin [2].

[Стек потока]

Для эффективной работы архитектуры Blackfin библиотеки VDK нуждаются в сохранении контекста потока, для чего служит стек потока. По этой причине размер стека потока делается на 45 слов длиннее, чем указано в свойствах типа потока на закладке Kernel. Поле Stack Size в окошке VDK Status (см. врезку "Отладочная информация Instrumented Build VDK") показывает размер памяти, который был выделен и для стека, и для сохранения контекста, так что этот размер отличается от значения размера стека потока, который был введен на закладке Kernel.

[Поддержка вложенности прерываний (Interrupt Nesting)]

VDK полностью поддерживает вложенные друг в друга прерывания:

• Скелетный код (шаблон) для обработчиков прерываний, который генерирует среда VisualDSP++ 5.0 IDDE для ISR, определяемых пользователем, включают инструкции, которые разрешают вложение прерываний, если это необходимо.

• Код VDK ISR API написан так, что полностью поддерживает реентерабельность (fully reentrant).

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

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

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

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

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

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

[Системный стек (System Stack)]

Thread stack. Есть 2 типа стека VDK: стек потоков (thread stack) и стек системы (system stack). Thread stack выделяется для каждого потока индивидуально, и для этих стеков память выделяется из кучи (heap). Использование прерываниями стека потока у процессоров Blackfin описано в следующей секции.

System stack используется для многих операций ядра, таких как инициализация VDK и управление переключением контекста (thread scheduling). Ниже описано, когда VDK использует system stack, и некоторые соображения для определения требований к системному стеку со стороны приложения:

• При запуске приложения (startup) system stack используется от адреса сброса системы до входа в функцию Run() первого запускаемого потока загрузки (boot thread). Этим обслуживаются операции инициализации рабочего окружения кода (C/C++ runtime environment), конструкторы глобальной переменной, инициализация VDK, конструкторы для потоков загрузки VDK C++, функция InitFunction() для потоков загрузки на C/ассемблере, и секция Init для любой загрузки объектов IO.

• Как только первый поток загрузки начал выполняться, system stack используется только для коротких периодов во время шедулинга потоков и для любых отложенных вызовов процедур (deferred procedure calls). VDK использует вызовы отложенных процедур для внутренних обновлений, таких как таймауты, и для некоторых сконфигурированных пользователем вызовов отложенных процедур, что включает:

   - Часть "activate" драйверов устройств VDK.
   - Любые системные службы для отложенных функций обратного вызова (deferred callbacks).

• Прерывания могут произойти, когда VDK использует system stack. Поэтому должно быть зарезервировано достаточно места в system stack, чтобы там уместились данные, сохраняемые при вызове любых ISR в системе (если любые прерывания могут быть вложенными, то требования к размеру системного стека увеличиваются). Это относится к прерываниям, написанным на языках C, C++ и ассемблере.

[Использование прерываниями стека потока]

Из-за того, что код всех потоков работает в режиме супервизора, нет автоматического переключения между указателями пользовательского и системного стека. Таким образом, все ISR выполняются с использованием стека текущего работающего потока, во время активности которого произошло прерывание. Это означает, что размер стека каждого потока должен иметь достаточно места как для самого потока, так и для требований обработчиков прерываний (ISR). Это также относится и к размеру стека потока ожидания (Idle thread), что можно сконфигурировать средствами среды разработки VisualDSP++ (для дополнительной информации см. онлайн-Help). Когда разрешено вложение прерываний друг в друга, то наступает самый худший сценарий для выделения места в памяти под стек, потому что требования к стеку от отдельных ISR могут складываться. Когда вложение прерываний запрещено, то для удовлетворения требований к размеру стеку со стороны ISR нужно учитывать только требование от потока, который больше всего занимает памяти в стеке. Это может быть одной из причин запрета вложенности прерываний.

[Латентность обработки прерываний]

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

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

[Обмен сообщениями между процессорами]

Двухядерный процессор ADSP-BF561 является единственным в семействе Blackfin, для которого "из коробки" предоставляется поддержка обмена сообщениями между ядрами (они могут рассматриваться как отдельные процессоры), multiprocessor messaging. Драйвер устройства, который использует два внутренние канала DMA (Internal Memory DMA: IMDMA0 и IMDMA1) для обмена между двумя ядрами, можно найти в качестве примера в каталоге установки VisualDSP++ 5.0 (см. каталоги Examples).

Из-за того, что каналы IMDMA поддерживают только память L1 и L2, необходимо быть осторожным в случаях, когда в приложении также используется внешняя память (SDRAM, это память уровня L3). Полезная нагрузка сообщений, перемещаемая через память, не может находиться во внешней памяти, чтобы её мог записать или прочитать драйвер устройства IMDMA, так что такая полезная нагрузка не может быть автоматически передана функциями маршалирования. Однако поскольку внешняя память видна обоими ядрами по одним и тем же адресам, обычно не требуется копировать полезную нагрузку между ядрами, когда она находится во внешней памяти. В правильно разработанном приложении есть возможность передать адрес и размер полезной нагрузки как немаршалируемый тип нагрузки, и получить доступ к её содержимому со стороны любого из ядер. Это будет наиболее эффективным способом обмена данными, если они находятся в L3.

Также из-за того, что каналы IMDMA поддерживают только память L1 и L2, стеки маршрутизируемых потоков (routing threads) должны быть выделены из кучи, которая находится в памяти L1 или L2 (как это по умолчанию задано в конфигурации VDK). В проектах Lwip (поддержка сетевого стека) the system heap по умолчанию находится в SDRAM, поэтому если требуется multiprocessor messaging, то пользователи должны сконфигурировать дополнительную кучу в L1 или L2, и модифицировать настройки на закладке Kernel свойств проекта VDK, чтобы стеки routing threads были выделены в куче, находящейся во внутренней памяти.

Обратите внимание, что нет когерентности кэша между двумя ядрами процессора ADSP-BF561. Таким образом, если разрешено кэширование, то любые регионы памяти, к которым осуществляется доступ обоими ядрами (это относится как к памяти L2, так и к внешней памяти, SDRAM L3) должны быть определены не кэшируемыми через таблицы CPLB. Для получения дополнительной информации, как конфигурировать CPLB, см. секцию "Caching and Memory Protection" в руководстве по компилятору и библиотекам для процессоров Blackfin [2].

Дополнительно стеки потоков для Routing Threads не должны размещаться во внешней памяти. Причина в том, что структуры буфера, используемые для передачи и приема пакетов сообщений сохраняются в стеке, и они должны размещаться в памяти L1 или L2.

[Переполнение стека]

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

Начиная с апдейта 9 для VisualDSP++ 5.0 и более новых версий компилятора Blackfin предоставляется возможность детектирования переполнений стека (для чего используется опция -stack-detect [8]). Этот функционал компилятора полагается на глобальную структуру, которая обновляется во всех местах для получения информации по использованию стека. В многопоточном приложении это означает необходимость обновления данных этой структуры всякий раз, когда происходит переключение контекста (передача управления на другой поток).

Чтобы не делать замедления переключения контекста, когда требуется детектировать переполнение стека, VDK предоставляет специальную версию своего переключения контекста, которая должна использоваться, если разрешена функция stack overflow detection.

Чтобы выбрать корректный тип переключения контекста (планировщика) для приложения, LDF-файл проекта должен быть обновлен и включать в себя следующих код (или его эквивалент для Вашего конкретного LDF-файла).

#ifdef _ADI_SOV_DETECTION
   TMK-BF532_sov.dlb,
#else
   TMK-BF532.dlb,
#endif

Вы можете найти этот код в LDF-файле по умолчанию каталога установки VisualDSP++ 5.0 update 9 (или более свежей версии). Если Вы не примените это изменение для LDF и разрешите детектирование переполнения стека, то приложение VDK будет падать в KernelPanic.

В случае, когда было детектировано переполнение стека, компилятор вызовет функцию adi_stack_overflowed, которая в случае VDK вызовет KernelPanic с кодом паники kStackCheckFailure.

Для дополнительной информации о том, как работает система определения переполнения стека компилятора Blackfin, см. руководство компилятора [2].

[Макросы ассемблера C/C++ ISR API]

В этой секции описаны макросы ассемблера и языка C/C++ для ISR API, которые позволяют обработчикам прерывания (Interrupt Service Routines, ISR) взаимодействовать с VDK. Эти расширения библиотеки VDK называются ISR API, и они представляют только малую часть функционала VDK, которую можно безопасно вызывать из кода уровня прерывания (interrupt level).

В приложениях VDK, код обработчиков прерываний (ISR) должен выполняться максимально быстро, чтобы оставлять как можно больше процессорного времени потокам или активации драйвера устройства. Принципиальная цель ISR API состоит в предоставлении коду (потока или драйвера) как можно больше процессорного времени. В VisualDSP++ 3.5 и более ранних релизах VDK предоставляет прямую поддержку для обработчиков ISR, написанных только на ассемблере. Начиная с релиза VisualDSP++ 4.0 библиотека VDK поддерживает также и ISR, написанные на ассемблере и на C/C++.

Написание ISR на ассемблере уменьшает трату ресурсов процессора на сохранение и восстановления состояния процессора, и на установку рабочего окружения C run-time на каждом входе в обработчик ISR. ISR на ассемблере сам отвечает за сохранение и восстановление используемых регистров (об этом заботится программист). Не может быть сделано никаких предположений о том, в каком состоянии оказывается процессор, когда начинает выполняться код ISR. Каждый макрос ассемблера ISR сохраняет и восстанавливает все регистры, которые он использует, и макросы безопасен для использования вложенных друг в друга прерываний.

Макросы ISR ассемблера:

Макрос Краткое описание
VDK_ISR_ACTIVATE_DEVICE_()  
VDK_ISR_CLEAR_EVENTBIT_()  
VDK_ISR_LOG_HISTORY_()  
VDK_ISR_POST_SEMAPHORE_()  
VDK_ISR_SET_EVENTBIT_()  

VDK_ISR_ACTIVATE_DEVICE_(VDK_IOID inID);

Запускает именованный драйвер устройства на выполнение перед возвратом в домен потоков. Параметр указывает драйвер, который будет запущен. Макрос вызывает запуск планировщика перед возвратом в домен потоков. Время выполнения постоянное.

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kBadIOID, если inID больше, чем максимальное количество объектов I/O, которое разрешено иметь в системе.

VDK_ISR_CLEAR_EVENTBIT_(VDK_EventBitID inEventBit);

Очищает значение указанного бита событий inEventBit путем установки его в лог. 0 (FALSE). Все очистки бит, которые происходят в домене прерываний, будут обработаны немедленно до возврата в домен потоков.

Если inEventBit в настоящее время установлен (1), то макрос вызывает запуск планировщика перед возвратом в домен потоков. Это позволяет заново вычислить значение все зависимых событий, и может привести к переключению контекста (смене активного потока). Время выполнения постоянное.

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kUnknownEventBit, если inEventBit больше, чем количество бит событий, используемых в системе.

VDK_ISR_LOG_HISTORY_(VDK_HistoryEnum
                     inEnum,
                     int inVal,
                     VDK_ThreadID inThreadID); 

Добавляет запись в буфер истории. Это NULL, если макроопределение VDK_INSTRUMENTATION_LEVEL_ установлено в 0 или 1. Значение 2 показывает использование библиотек с полной инструментальной поддержкой. Для дополнительной информации см. онлайн-Help. 

Параметры: inEnum это значение перечисления для этого типа события (для дополнительной информации см. описание перечисления HistoryEnum). inVal это информация, смысл которой зависит от перечисления. inThreadID это идентификатор потока, который будет сохранен вместе с событием истории.

Не вызывает запуск планировщика, время выполнения постоянное.

VDK_ISR_POST_SEMAPHORE_(VDK_SemaphoreID inSemaphoreID); 

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

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

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kUnknownSemaphore, если inSemaphoreID больше, чем максимальное количество семафоров, которое разрешено иметь в системе.

VDK_ISR_SET_EVENTBIT_(VDK_EventBitID inEventBit) 

Устанавливает значение inEventBit (см. EventBitID) в лог. 1 (TRUE). Все установки битов, которые происходят в домене прерываний, будут обработаны немедленно, до возврата в домен потоков. 

Если inEventBit в настоящее время очищен (0), то макрос вызовет запуск планировщика перед возвратом в домен потоков. Это позволяет заново вычислить значение все зависимых событий, и может привести к переключению контекста (смене активного потока). Время выполнения постоянное.

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kUnknownEventBit, если inEventBit больше, чем количество бит событий, используемых в системе.

Написание ISR на C/C++ может упростить его кодирование. Однако следует помнить, что это делается ценой снижения производительности, связанным с тем, что выполняемый ISR написан на C/C++. Кроме того, следует заметить, что в основном библиотека run-time не безопасная для использования в коде ISR (not interrupt-safe), и поэтому стандартные библиотеки С/С++ нельзя использовать в ISR (подробнее см. руководство по компилятору и библиотекам [2]).

VDK API, которое можно вызывать из C/C++ ISR, следующее:

Макрос Краткое описание
C_ISR_ActivateDevice()  
C_ISR_ClearEventBit()  
C_ISR_PostSemaphore()  
C_ISR_SetEventBit()  

//Прототип на C:
void VDK_C_ISR_ActivateDevice(VDK_IOID inID);
 
//Прототип на C++:
void VDK::C_ISR_ActivateDevice(VDK::IOID inID); 

Запускает именованный драйвер устройства на выполнение перед возвратом в домен потоков. Параметр указывает драйвер, который будет запущен. Вызывает запуск планировщика перед возвратом в домен потоков. Время выполнения постоянное.

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kBadIOID, если inID больше, чем максимальное количество объектов I/O, которое разрешено иметь в системе.

//Прототип на C:
void VDK_C_ISR_ClearEventBit(VDK_EventBitID inEventBitID);
 
//Прототип на C++:
void VDK::C_ISR_ClearEventBit(VDK::EventBitID inEventBitID);

Очищает значение указанного бита событий inEventBit путем установки его в лог. 0 (FALSE). Все очистки бит, которые происходят в домене прерываний, будут обработаны немедленно до возврата в домен потоков.

Если inEventBit в настоящее время установлен (1), то вызывает запуск планировщика перед возвратом в домен потоков. Это позволяет заново вычислить значение все зависимых событий, и может привести к переключению контекста (смене активного потока). Время выполнения постоянное.

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kUnknownEventBit, если inEventBit больше, чем количество бит событий, используемых в системе.

//Прототип на C:
void VDK_C_ISR_PostSemaphore(VDK_SemaphoreID inSemaphoreID);
 
//Прототип на C++:
void VDK::C_ISR_PostSemaphore(VDK::SemaphoreID inSemaphoreID); 

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

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

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kUnknownSemaphore, если inSemaphoreID больше, чем максимальное количество семафоров, которое разрешено иметь в системе.

//Прототип на C:
void VDK_C_ISR_SetEventBit(VDK_EventBitID inEventBitID);
 
//Прототип на C++:
void VDK::C_ISR_SetEventBit(VDK::EventBitID inEventBitID); 

Устанавливает значение inEventBit (см. EventBitID) в лог. 1 (TRUE). Все установки битов, которые происходят в домене прерываний, будут обработаны немедленно, до возврата в домен потоков. 

Если inEventBit в настоящее время очищен (0), то вызовет запуск планировщика перед возвратом в домен потоков. Это позволяет заново вычислить значение все зависимых событий, и может привести к переключению контекста (смене активного потока). Время выполнения постоянное.

Обработка ошибок для сборки с полной инструментальной поддержкой: переход в KernelPanic с кодом паники g_KernelPanicCode / kISRError и g_KernelPanicError / kUnknownEventBit, если inEventBit больше, чем количество бит событий, используемых в системе.

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

В отличие от других операционных систем реального времени (real-time operating system, RTOS), 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().

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

[Ссылки]

1. VisualDSP++ 5.0 Kernel (VDK) User’s Guide site:analog.com.
2. VisualDSP++ 5.0 C/C++ Compiler and Libraries Manual site:analog.com.
3. C/C++ Compiler Manual for SHARC Processors site:analog.com.
4. VisualDSP++ 5.0 Linker and Utilities Manual site:analog.com.
5. ADSP-BF538: обработка событий (прерывания, исключения).
6. Blackfin: система команд (ассемблер) - часть 1.
7. VDK: драйверы устройств и системные службы процессоров Blackfin.
8. Опции командной строки компилятора Blackfin.
9. VDK: поток ожидания (Idle Thread).