VDK: интерфейс ввода/вывода |
Добавил(а) microsin |
Интерфейс ввода/вывода (I/O interface) предоставляет механизм для создания интерфейса между внешним окружением и приложением VDK. В среде разработки VisualDSP++ 5.0 для конструирования I/O интерфейса могут использоваться только объекты драйвера устройства (device driver). Для процессоров Blackfin модель драйверов устройств (VDK device driver model) устарела в пользу модели драйвера системных служб (system services device driver model). Модель VDK device driver model все еще используется для поддержки I/O процессоров SHARC, а также для обмена сообщениями между ядрами многопроцессорной системы (двухядерные процессоры Blackfin ADSP-BF561). [I/O Templates] Шаблоны I/O (I/O templates) аналогичны типам для потоков (thread types). I/O templates используются для инстанциации объектов ввода/вывода (I/O objects). В среде разработки VisualDSP++ 5.0 для драйверов устройств доступны только типы шаблонов I/O (таким образом, доступны только классы объектов I/O). Чтобы создать драйвер устройства, в проект VDK должен быть добавлен объект ввода/вывода загрузки (boot I/O object) с использованием шаблона драйвера устройства (device driver template). Вы можете отличать отдельные экземпляры одного и того же драйвера устройства. Дополнительную информацию см. в разделе "Init". [Драйверы устройства (Device Drivers)] Роль драйвера устройства (device driver) состоит в том, чтобы позволить разработчику программы абстрагироваться от подробностей аппаратной реализации. Например, программист разрабатывает фильтр с конечной импульсной характеристикой (Finite Impulse Response, FIR), и ему не нужно понимать запутанность подключения к преобразователю сигналов (АЦП), чтобы сосредоточиться на алгоритме FIR. С таким методом реализации программное обеспечение потом может быть использовано на разных платформах, где реализация аппаратуры может отличаться. Менеджер Коммуникаций (Communication Manager) управляет драйверами устройств в VDK. С использованием Communication Manager API можно поддерживать уровни абстракции между драйверами устройств, обработчиками прерываний и выполняемыми потоками. В этой статье подробно описано, как организован Communication Manager. Выполнение кода (Execution). Драйверы устройства и подпрограммы обработки прерываний очень сильно друг с другом связаны. Обычно разработчики DSP предпочитают наиболее критичный код писать на ассемблере. Communication Manager разработан так, что Вы можете сохранить свои обработчики прерывания на ассемблере (их части, наиболее критичные по времени выполнения), и интерфейс и управление ресурсами для устройства сделать на языке высокого уровня, не жертвуя скоростью выполнения. Communication Manager пытается свести к минимуму количество переключений контекста, чтобы выполнять код управления в подходящие интервалы времени, и сохранить порядок обработки приоритетов работающих потоков, когда поток использует устройство. Однако Вам нужно четко понимать архитектуру Communication Manager, чтобы Вы могли написать свой собственный драйвер. Имеется только один интерфейс для драйвера устройства - через функцию диспетчера. Функция диспетчера вызывается, когда устройство инициализировано, когда поток использует устройство (открывает/закрывает (open/close), читает/пишет (read/write), управляет (control)), или когда ISR передает данные в устройство или из него. Функция диспетчера обрабатывает запрос и выполняет возврат. Драйверы устройства не должны блокировать выполнение на ожидании (pend), когда обрабатывают запрос инициализации или запрос на большее количество данных от ISR. Однако драйвер устройства может выполнить блокировку, когда обрабатывает запрос потока, и соответствующий ресурс пока не готов или недоступен. Запрошенные ядром инициализация драйвера устройства и запросы ISR, обрабатываются в критических регионах, так что их выполнение не должно быть реентерабельным (reentrant). Запрос уровня потока должен защитить глобальные переменные с помощью критических или необслуживаемых регионов. Параллельная планировка доменов (Parallel Scheduling Domains). В этой секции обсуждение сфокусировано на уникальной роли драйверов устройств в архитектуре VDK. Понимание сущности драйверов устройств требует некоторого понимания времени и метода, с которым вовлекается выполнение кода драйвера устройства. Приложения VDK могут включать 2 домена кода - домен потоков (thread domain) и домен ISR (домен обработчиков прерывания, см. рис. 3-18). Это разделение не является произвольной или ненужной абстракцией. Аппаратная архитектура процессора, как и архитектура программного обеспечения kernel берут на вооружение это разделение кода. Вы должны учитывать это разделение, когда разрабатываете приложение и распространяете алгоритм по различным частям кода. Рис. 3-18. Домены параллельного планирования выполнения кода. Планирование выполнения потоков базируется на их приоритетах, и на порядке, в котором они попали в очередь готовности (ready queue). Планирующая часть ядра (планировщик, scheduler) отвечает за выбор потока для запуска. Однако у планировщика нет полного контроля за вычислительным ресурсом процессора. Работа планировщика может быть параллельно вытеснена более высокоприоритетным планировщиком - прерыванием и аппаратным исключением (exception). Когда обрабатываются прерывания или исключения, приоритеты потоков временно не учитываются. Позиция потоков в очереди готовности снова становится значимой только аппаратура передает обратно управление программному планировщику. У каждого из доменов есть свои достоинства и недостатки, диктующие применимость выполнения того или иного кода в соответствующей среде. Планировщик домена потоков вовлекается, когда потоки помещаются в очередь готовности или извлекаются оттуда. Каждый поток имеет собственный стек, и может быть написан на высокоуровневом языке программирования. Потоки всегда выполняются в режиме "супервизора" (supervisor), или в режиме ядра (kernel mode) - в том случае, если процессор распознает такие различия. Потоки реализуют алгоритм приложения, и распоряжаются процессорным временем на основе необходимости первоочередного выполнения самого высокоприоритетного действия. Планирование в домене прерываний отличается от планирования домена потоков тем, что в домене прерываний есть самый высокий приоритет в масштабе всей системы. Любой находящийся в состоянии готовности (ready) ISR получает преимущество перед любым потоком, находящимся в состоянии готовности (если выполнение кода находится вне критического региона), и эта форма планирования реализована аппаратно. Если ISR написан на ассемблере, то он должен вручную восстанавливать (на выходе ISR) любые регистры, которые были модифицированы в коде ISR. ISR отвечает за асинхронные периферийные устройства только на самом низком уровне. Подпрограмма ISR должна выполнять только самые необходимые действия, критичные по времени выполнения, которые нужны для того, чтобы обеспечить отсутствие потерь данных, которые могли бы быть, если бы код выполнялся недостаточно быстро. Все другие действия должны быть перемещены в домен потоков, под управление планировщика ядра, и выполняться в соответствии с назначенными им приоритетами. Переход из домена потоков в домен прерываний прост и автоматизирован, но возврат в домен потоков может быть более сложным. Если очередь готовности (ready queue) во время выполнения домена прерываний не поменялась, то планировщик запускать не нужно, когда ему возвращается управление над системой. Прерванный поток может немедленно возобновить свое выполнение. Если же очередь готовности поменялась, то планировщик должен далее определить, изменился ли самый высокоприоритетный поток. Если он поменялся, то планировщик должен инициировать переключение контекста (запустить на выполнение другой поток). Драйверы устройства заполняют промежуток между двумя планируемыми доменами. Их код не является ни кодом потока, ни кодом ISR, и их выполнение не планируется напрямую ни ядром, ни контроллером прерывания. Драйверы устройства реализованы как объекты C++, и работают на стеке текущего запущенного потока. Однако они не захвачены во владение потоком, и могут конкурентно быть использованы несколькими потоками. Использование драйверов устройства. С точки зрения потока есть 5 функциональных интерфейсов до драйверов устройства: OpenDevice(), CloseDevice(), SyncRead(), SyncWrite() и DeviceIOCtl(). Имена вызовов API сами объясняют свое значение, так как потоки работают с драйверами устройства как с черными ящиками. На рис. 3-19 показан интерфейс драйвера устройства. Рис. 3-19. Device Driver API. Поток использует устройство путем его открытия (open), его чтения (read) и/или записи (writing) в него, после чего устройство закрывается потоком (close). Функция DeviceIOCtl() используется для отправки управляющих информационных сообщений, специфичных для устройства. Каждый вызов API это стандартная функция C/C++, которая работает на стеке вызывающего потока, и управление возвращается в вызывающий поток, когда функция завершает свою работу. Однако в случае, когда у драйвера устройства нет необходимого ресурса, одна из этих функций может привести поток к выводу из очереди готовности с целью блокировки на ожидании сигнала, наподобие семафора (semaphore) или события (event), называемого флагом устройства (device flag). Подпрограммы ISR имеют только один вызов API, связанный с драйверами устройства: VDK_ISR_ACTIVATE_DEVICE_(). Этот макрос не вызов функции, и поток вычислений не переходит из ISR в драйвер устройства и обратно. Вместо этого макрос устанавливает флаг, который показывает, что должна быть выполнена подпрограмма "активирования" драйвера устройства после того, как будут обслужены все прерывания. Оставшиеся две функции API, PendDeviceFlag() и PostDeviceFlag(), обычно вызываются из самого драйвера устройства. Например, вызов из потока SyncRead() может привести к тому, что драйвер устройства вызовет PendDeviceFlag() если пока нет доступных данных. Это заставило бы поток блокировать свое выполение на флаге устройства, пока этот флаг не будет установлен другим фрагментом кода драйвера устройства, который предоставляет данные. Как другой пример, когда происходит прерывание из-за того, что буфер входных данных заполнен, ISR может переместить указатель так, чтобы устройство начало заполнять пустой буфер перед вызовом VDK_ISR_ACTIVATE_DEVICE_(). Подпрограмма активации драйвера устройства может ответить выставлением флага устройства и переходом потока в очередь готовности, так чтобы запланировать обработку поступивших новых данных. Функция диспетчера (Dispatch Function). Функция диспетчера является ядром любого драйвера устройства. Эта функция принимает 2 параметра и возвращает void* (смысл возвращаемого значения зависит от входных значений). Функция диспетчера A dispatch для драйвера устройства декларируется следующим образом. Для кода драйвера на языке C: void* MyDevice_DispatchFunction(VDK_DispatchID inCode, VDK_DispatchUnion inData); Для кода драйвера на языке C++: void* MyDevice::DispatchFunction(VDK::DispatchID inCode, VDK::DispatchUnion &inData); Первый параметр это перечисление, которое указывает, какая функция диспетчера должна быть вызвана: enum VDK_DispatchID
{
VDK_kIO_Init,
VDK_kIO_Activate,
VDK_kIO_Open,
VDK_kIO_Close,
VDK_kIO_SyncRead,
VDK_kIO_SyncWrite,
VDK_kIO_IOCtl
};
Второй параметр это объединение, значение которого зависит от переданного значения из перечисления: union VDK_DispatchUnion { struct OpenClose_t { void **dataH; char *flags; /* используется только для kIO_Open */ }; struct ReadWrite_t { void **dataH; VDK_Ticks timeout; unsigned int dataSize; int *data; }; struct IOCtl_t { void **dataH; void *command; char *parameters; }; struct Init_t { void *pInitInfo; }; }; Значения в перечислении (union) являются допустимыми только тогда, когда перечисление указывает, что функция диспетчера была вызвана из домена потоков (kIO_Open, kIO_Close, kIO_SyncRead, kIO_SyncWrite, kIO_IOCtl). Функция диспетчера может быть структурирована следующим образом. На языке C: void* MyDevice_DispatchFunction (VDK_DispatchID inCode, VDK_DispatchUnion inData) { switch(inCode) { case VDK_kIO_Init: /* Инициализация устройства */ case VDK_kIO_Activate: /* Получение для ISR большего количества готовых данных */ case VDK_kIO_Open: /* Поток хотел бы открыть устройство... */ /* Выделить память и подготовить что-то еще */ case VDK_kIO_Close: /* Поток закрывает соединение с устройством...*/ /* Происходит освобождение всей выделенной памяти, и производятся какие-нибудь другие действия */ case VDK_kIO_SyncRead: /* Поток выполняет чтение из устройства */ /* Возврат unsigned int для количества прочитанных байт */ case VDK_kIO_SyncWrite: /* Поток выполняет запись в устройство */ /* Возврат unsigned int для количества записанных байт */ case VDK_kIO_IOCtl: /* Поток выполняет какие-то специфичные для устройства действия: */ default: /* Недопустимый код DispatchID */ return 0; } } На языке C++: void* MyDevice::DispatchFunction (VDK::DispatchID inCode, VDK::DispatchUnion &inData) { switch(inCode) { case VDK::kIO_Init: /* Инициализация устройства */ case VDK::kIO_Activate: /* Получение для ISR большего количества готовых данных */ case VDK::kIO_Open: /* Поток хотел бы открыть устройство... */ /* Выделить память и подготовить что-то еще */ case VDK::kIO_Close: /* Поток закрывает соединение с устройством...*/ /* Происходит освобождение всей выделенной памяти, и производятся какие-нибудь другие действия */ case VDK::kIO_SyncRead: /* Поток выполняет чтение из устройства */ /* Возврат unsigned int для количества прочитанных байт */ case VDK::kIO_SyncWrite: /* Поток выполняет запись в устройство */ /* Возврат unsigned int для количества записанных байт */ case VDK::kIO_IOCtl: /* Поток выполняет какие-то специфичные для устройства действия: */ default: /* Недопустимый код DispatchID */ return 0; } } Ниже обсуждается каждый из отдельных случаев обработки функции диспетчера. Init (инициализация). Функция диспетчера вызывается с параметром VDK_kIO_Init для кода устройства стиля C и VDK::kIO_Init для кода устройства стиля C++ - в момент загрузки системы (system boot time). В этот момент должны быть настроены все зависящие от устройства структуры данных и системные ресурсы. Функция инициализации вызывается из критического региона кода, и не должна делать любые вызовы API, которые диспетчеризуют ошибку или могут привести к блокированию. Все функции инициализации драйвера устройства выполняются до того, как будет запущен любой из потоков, они могут использоваться, чтобы выполнить любой требуемый код инициализации. Это справедливо даже если тип драйвера устройства является пока не завершен (заглушка), назначение которого только лишь содержать некий код инициализации. Переданное в функцию диспетчера объединение определено как Init_t из VDK_DispatchUnion. Init_t определяется следующим образом. struct Init_t { void *pInitInfo; }; Init_t.pInitInfo: указатель (тип void*) на значение, определенное в поле Initializer на закладке Kernel для объектов I/O загрузки (см. раздел "Параметризация драйвера устройства"). Open и Close. Когда поток открывает или закрывает устройство вызовом OpenDevice() или CloseDevice(), функция диспетчера вызывается с VDK_kIO_Open или VDK_kIO_Close. Функция диспетчера вызывается из домена потока, так что любые основанные на стеке переменные являются локальными для этого потока. Доступ к общим данным (тем данным, к которым может быть получен доступ из потоков и/или прерываний, и/или из функций активации драйвера устройства) должен соответствующим образом защищен использованием необслуживаемых регионов кода (unscheduled region), критических регионов кода (critical region), или другими средствами. Когда поток вызывает функцию диспетчера, пытаясь открыть или закрыть устройство, API передается union в функцию диспетчера устройства, и его значение определено как OpenClose_t из VDK_DispatchUnion. OpenClose_t определяется следующим образом. struct OpenClose_t { void **dataH; char *flags; /* используется только для kIO_Open */ }; OpenClose_t.dataH: указатель на зависящее от потока место в памяти, которое драйвер устройство может использовать для хранения любых относящихся к потоку ресурсов. Например, поток может выделить память (через вызов malloc) для структуры, которая описывает состояние потока, связанного с устройством. Указатель на структуру может быть сохранен в *dataH, который затем будет доступен для любого другого вызова диспетчера, вовлеченного этим потоком. Драйвер устройства может освободить это место, когда потоком будет вызвана функция CloseDevice(). OpenClose_t.flags: второй параметр, переданный в вызов OpenDevice(), предоставляет для функции диспетчера значение OpenClose_t.flags. Это используется для передачи любой зависящей от устройства информации флагов, относящейся к открытию устройства. Обратите внимание, что эта часть объединения не используется в вызове CloseDevice(). В любой момент времени может быть открыто не более 8 устройств на поток. Read и Write. Поток, которому нужно читать в открытое устройство или писать в него, вызывает SyncRead() или SyncWrite() соответственно. Функция диспетчера вызывается в домене потока и работает на стеке потока. Эти вызовы функций вызывают функцию диспетчера с параметрами, переданными API в VDK_DispatchUnion, и флаги VDK_kIO_SyncRead или VDK_kIO_SyncWrite. Структура ReadWrite_t определена следующим образом: struct ReadWrite_t { void **dataH; VDK::Ticks timeout; unsigned int dataSize; int *data; }; ReadWrite_t.dataH: место в памяти, зависящее от потока, которое передается функции диспетчера при открытии устройства в вызове OpenDevice(). Эта переменная может использоваться для хранения указателя на структуру данных, специфичных для потока, описывающих состояние потока, когда он осуществляет обмен с устройством. ReadWrite_t.timeout: Количество времени в тиках системы, которое поток готов ожидать завершения вызовов SyncRead() или SyncWrite(). Если требуется такое поведение с таймаутом, то оно должно быть реализовано с использованием значения ReadWrite_t.timeout как аргумента соответствующего вызова PendDeviceFlag() в функции диспетчера. ReadWrite_t.dataSize: количество данных, которое поток читает из устройства или записывает в устройство. ReadWrite_t.data: указатель на место в памяти, куда поток записывает данные (при чтении), или откуда берет данные (при записи). Наподобие вызовов функции диспетчера устройства для открытия и закрытия, вызовы для чтения и записи не защищены критическим или необслуживаемым регионом кода. Если драйвер устройства обращается к глобальным структурам данных при чтении или записи, то доступ должен быть защищен критическим или необслуживаемым регионом кода. Для дополнительной информации по поводу регионов и ожидания см. раздел "Драйверы устройства (Device Drivers)". IOCtl. VDK предоставляет потокам интерфейс для управления параметрами устройства с помощью DeviceIOCtl() API. Когда поток вызывает DeviceIOCtl(), функция устанавливает некоторые параметры и вызывает функцию диспетчера указанного драйвера со значением VDK_kIO_IOCtl и VDK_DispatchUnion, установленном как IOCtl_t. IOCtl_t определяется следующим образом. struct IOCtl_t { void **dataH; void *command; char *parameters; }; IOCtl_t.dataH: место в памяти, зависящее от потока, которое передается функции диспетчера при открытии устройства вызовом OpenDevice(). Эта переменная может использоваться для сохранения указателя на относящуюся к потоку структуру данных, детализирующую информацию по состоянию потока, когда он общается с устройством. IOCtl_t.command: относящийся к устройству указатель (второй параметр функции DeviceIOCtl()). IOCtl_t.parameters: относящийся к устройству указатель (третий параметр функции DeviceIOCtl()). Наподобие чтения/записи и открытия/закрытия, вызов функции диспетчера для IOCtl не защищен критическим или необслуживаемым регионом кода. Если устройство обращается к глобальным структурам данных, то доступ должен быть защищен критическим или необслуживаемым регионом кода. Activate. Часто драйвер устройства нуждается в ответе на изменения состояния, которые были произведены обработчиками ISR. Функция диспетчера устройства вызывается со значением VDK_kIO_Activate в некоторый момент времени после того, как ISR вызвал макрос VDK_ISR_ACTIVATE_DEVICE_(). Когда ISR вызывает VDK_ISR_ACTIVATE_DEVICE_(), устанавливается флаг, показывающий, что устройство было активировано, и срабатывает на запуск программное прерывание с низким уровнем приоритета (см. раздел "Reschedule ISR" [2]). Когда планировщик получает управление через программное прерывание низкого уровня приоритета, то вызывается функция диспетчера устройства со значением VDK_kIO_Activate. Часть функции диспетчера, относящаяся к активации, должна поддерживать публикацию сигналов, так чтобы потоки, которые ожидают определенных состояний устройства, могли продолжить свою работу. Например предположим, что ISR цифро-аналогового преобразователя дошел до конца своего буфера данных. ISR вызывает VDK_ISR_ACTIVATE_DEVICE_() с IOID драйвера устройства. Когда функция диспетчера устройства вызывается с VDK_kIO_Activate, устройство публикует флаг устройства или семафор, который возобновляет планирование любых потоков, которые ожидают события для необходимости обновления буфера. Примечание: вызовы API-функций PostDeviceFlag(), PostSemaphore(), PushCriticalRegion() и PopCriticalRegion() относятся только к VDK API, которые можно безопасно вызывать из функции активации. Параметризация драйвера устройства (Device Driver Parameterization). Когда используется больше одного экземпляра определенного драйвера устройства, то может быть передано значение initializer в каждый объект ввода/вывода загрузки (boot I/O object), чтобы предоставить уникальный номер экземпляра для драйвера. Для этого используется поле Initializer на закладке Kernel свойств проекта, чтобы назначить идентификатор для объектов ввода/вывода загрузки (подробности см. в online Help). Значение initializer передается в функцию диспетчера инициализации через Init_t.pInitInfo. Это указатель (типа void*) на значение, определенное в поле Initializer. Значение может быть получено так: int initializer = *((int *)inUnion.Init_t.pInitInfo); Значение должно быть сохранено в приватных данных, принадлежащих устройству; например, в члене класса. [Флаги устройства (Device Flags)] Флаги устройства это примитивы, используемые для синхронизации, подобно семафорам (semaphore), событиям (event) и сообщениям (message), но флаги устройства имеют специальную связь с драйверами устройства. Как и с семафорами и событиями, поток может находится в состоянии ожидания (pend) определенного флага устройства. Это означает, что поток ждет, пока флаг не будет выставлен (post, опубликован) драйвером устройства. Публикация обычно случается из функции активации функции диспетчера драйвера устройства. Ожидание на флаге устройства (Pending on a Device Flag). Когда поток приостановлен на ожидании (pend) флага устройства (в отличие от семафоров, событий и сообщений), то поток всегда блокируется. Поток ждет, пока вызовом другой функции диспетчера не будет выставлен нужный флаг. Когда флаг опубликован, то все потоки, ожидающие этого флага устройства, перемещаются в очередь готовности VDK (ready queue). Начиная с публикации флага через PostDeviceFlag() API с перемещением неопределенного количества потоков в очередь готовности, момент вызова ожидающего на флаге потока точно не определен. Для дополнительной информации по поводу публикации флагов устройства см. раздел "Posting a Device Flag". Правила для ожидания флагов устройства строги по сравнению с обработкой других типов сигналов. "Стек" критических регионов должен быть глубиной точно в 1 уровень, когда поток ждет флага устройства. Другими словами, когда прерывания разрешены, вызов PushCriticalRegion() должен быть выполнен точно только 1 раз до вызова PendDeviceFlag() из потока. Причина для этого условия становится понятной, если рассмотреть причину ожидания. Поток ждет на флаге устройства, когда он ждет условия, установленного из ISR. Однако Вы должны войти в критический регион кода перед проверкой любого условия, которое могло быть изменено из ISR, чтобы гарантировать, что прочитанное значение является достоверным. Кроме того, PendDeviceFlag() выталкивает один раз из стека критических регионов, эффективно балансируя предыдущие вызовы PushCriticalRegion(). Например, типичный драйвер устройства использует флаги устройства следующим образом. VDK_PushCriticalRegion(); while(should_loop != 0) { /* ... */ /* доступ к глобальным структурам данных */ /* ... */ /* ожидание некоторого состояния устройства */ VDK_PendDeviceFlag(); /* Должен снова войти в критический регион кода */ VDK_PushCriticalRegion(); } VDK_PopCriticalRegion(); На рис. 3-20 показан процесс ожидания флага устройства. Рис. 3-20. Ожидание флага устройства (Device Flag). Публикация флага устройства (Posting a Device Flag). Точно так же, как и семафоры и сообщения, можно опубликовать флаг устройства (post). Функция диспетчера устройства публикует флаг устройства путем вызова PostDeviceFlag(). В отличие от семафоров и сообщений, вызов перемещает все потоки, ожидающие флаг устройства, в очередь готовности, и продолжает выполнение. Как только произошел возврат из PostDeviceFlag(), последующие вызовы PendDeviceFlag() приведут к блокировке потока (как это было сделано ранее). Имейте в виду, что PostDeviceFlag() API не диспетчеризирует обработку любых ошибок. Причина в том, что эта функция API обычно вызывается из функции диспетчера, когда функция диспетчера была вызвана с VDK_kIO_Activate. Это происходит потому, что функция диспетчера устройства работает на стеке kernel, когда она вызывается с VDK_kIO_Activate, а не на стеке потока. На рис. 3-21 иллюстрируется процесс публикации флага устройства. Рис. 3-21. Публикация (posting) флага устройства (Device Flag). [Основные замечания] Придерживайтесь следующих советов, когда пишете код драйверов устройства. Хотя многие из советов также относятся к потокам, они заслуживают отдельного упоминания в связи с драйверами устройства. Переменные. Драйверы устройства и обработчики ISR тесно связаны друг с другом. Поскольку обработчики ISR функция диспетчера обращаются к одним и тем же переменным, декларируйте эти переменные в подпрограмме C/C++ драйвера устройства и обращайтесь к ним в коде ISR как к внешним (через декларацию extern) переменным. Когда эти переменные декларируются в исходном файле C/C++, то нужно их декларировать с ключевым словом volatile, чтобы гарантировать, что оптимизатор компилятора знает: значение этих переменных может быть изменено внешним кодом в любой момент времени. Дополнительно следует позаботиться о том, что переменные, определенные в коде C/C++, корректным образом упомянуты в коде ISR на ассемблере с использованием декорации имен (decorated/mangled name). Критические / необслуживаемые регионы кода (Critical/Unscheduled Regions). Поскольку многие структуры данных и переменные, связанные с драйвером устройства, используются совместно с другими потоками и обработчиками ISR, то доступ к ним должен быть защищен критическими регионами и необслуживаемыми регионами. Критические регионы не дают обработчикам ISR неожиданно модифицировать структуры данных, и необслуживаемые регионы защищают модификацию структур данных со стороны других потоков. Когда осуществляется ожидание флагов устройства, нужно позаботиться о том, что код находится в корректном регионе. Флаги устройства должны ожидаться из в не вложенного критического региона, как это обсуждается в разделе "Ожидание на флаге устройства". [Ссылки] 1. VDK I/O Interface site:analog.com. |