Миро Самек. Построение простых систем на ARM-контроллерах с использованием инструментов GNU (перевод)


Часть 7

В этой части описываются принципы блокировки и разрешения прерываний безопасные как для применения в обработчиках IRQ и FIQ прерываний, так и для кода приложения (вызываемого из "main()").

Возможно, вы никогда не думали, что вопрос блокировки прерываний может потребовать целой главы, но архитектура ARM иногда приводит к серьёзным сложностям. Рекомендуется ознакомиться со статьями: "Что происходит, если прерывание возникает в момент его запрещения" фирмы ARM [11] и "Запрет прерываний на уровне процессора" фирмы Atmel [12].


7.1 Блокировка прерываний - описание проблемы

В простых программных конструкциях без операционной системы и многозадачности (bare metal) приоритетная задача (ISR) общается с фоновым процессом (цикл "main()") через общие переменные.

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

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

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

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

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

Резюмируя, простые (bare-metal) проекты на ARM, предназначенные для повседневной действительности, требуют единого механизма блокировки и разрешения прерываний, безопасного при использовании как на уровне приложения, так и в обработчиках IRQ и FIQ прерываний.


7.2 Стратегия сохранения и восстановления состояния системы прерываний

Подход к блокировке прерываний, обладающий всеми требуемыми свойствами, основывается на сохранении и восстановлении состояния системы прерываний. Это общепринятый подход, используемый, к примеру, в операционной системе реального времени "VxWorks" (парные функций "intLock()" и "intUnlock()", описанние которых можно найти на сайте Stanford SLAC). Приведённый ниже отрывок кода ("isr.c") показывает как используется такой тип критических секций:

 

      void eventFlagSet(uint8_t flag) {
(1)         ARM_INT_KEY_TYPE int_lock_key;

(2)         ARM_INT_LOCK(int_lock_key);
            l_eventFlags |= (1 << flag);
(3)         ARM_INT_UNLOCK(int_lock_key);
      }
										

(1) Для начала, надо объявить временную переменную (обычно стековую или регистровую), которая будет сохранять состояние прерываний внутри критической секции.

Из соображений переносимости тип состояния прерываний объявляется в виде макроса "ARM_INT_KEY_TYPE", который вместе с макросами для блокировки и разрешения прерываний определён в файле "arm_exc.h", включённом в код, сопровождающий статью.

(2) Затем, используя другой макрос "ARM_INT_LOCK()", который сохраняет состояние в переменной "int_lock_key", прерывания блокируются.

(3) И, наконец, макрос "ARM_INT_UNLOCK()" возвращает систему прерываний в состояние, соответствующее таковому на момент вызова "ARM_INT_LOCK()".

Такой подход допускает вложенность критических секций, так как состояние флагов прерываний сохраняется на протяжении критической секции во временной переменной. Другими словами, по выходе из критической секции прерывания разрешаются макросом "ARM_INT_UNLOCK()" только в том случае, если они были разрешены в момент вызова парного макроса "ARM_INT_LOCK()". Соответственно, прерывания останутся блокированными после "ARM_INT_UNLOCK()", если они были блокированы до вызова "ARM_INT_LOCK()".


7.3 Реализация критических секций с помощью GNU gcc

Макросы "ARM_INT_KEY_TYPE", "ARM_INT_LOCK()" и "ARM_INT_UNLOCK()" объявлены в заголовочном файле "arm_exc.h", включённом в состав кода, сопровождающего статью, и показаны в листинге 7.1.

 
Листинг 7.1 Реализация критических секций

(1)   #define ARM_INT_KEY_TYPE         int

(2)   #ifdef __thumb__

(3)         #define ARM_INT_LOCK(key_)   ((key_) = ARM_int_lock_SYS())
(4)         #define ARM_INT_UNLOCK(key_) (ARM_int_unlock_SYS(key_))

(5)         ARM_INT_KEY_TYPE ARM_int_lock_SYS(void);
(6)         void ARM_int_unlock_SYS(ARM_INT_KEY_TYPE key);

(7)   #else

(8)         #define ARM_INT_LOCK(key_)   do { \
(9)               asm("MRS %0,cpsr" : "=r" (key_)); \
(10)              asm("MSR cpsr_c,#(0x1F | 0x80 | 0x40)"); \
(11)        } while (0)

(12)        #define ARM_INT_UNLOCK(key_) asm("MSR cpsr_c,%0" : : "r" (key_))

      #endif
										

Ниже поясняются ключевые моменты реализации:

(1) Макрос "ARM_INT_KEY_TYPE" объявляет тип состояния системы прерываний, сохраняемого на время работы критической секции. В случае процессора ARM, сотояние системы прерываний это значение регистра "CPSR" (для ARM gcc - 32-битная переменная типа int).

(2) GNU gcc для ARM определяет макрос "__thumb__", если вызывается с опцией "-mthumb" для компиляции кода в 16-битном режиме THUMB.

(3) Набор инструкций THUMB не включает коды команд "MSR" и "MRS", которые позволяют работать с битами прерываний регистра "CPSR".

По этой причине макрос "ARM_INT_LOCK()" вызывает функцию "ARM_int_lock_SYS()", скомпилированную под набор инструкций ARM, которая блокирует прерывания и возвращает значение регистра "CPSR" до блокировки прерываний. Благодаря опции "-mthumb-interwork" gcc компилятор и компоновщик добавляют соответствующий вызов "THUMB-to-ARM" либо "ARM-to-THUMB" автоматически (см. четвёртую часть статьи).

(4) Макрос "ARM_INT_UNLOCK()" определён как вызов функции "ARM_int_unlock_SYS()", восстанавливающей значение регистра "CPSR" из входного аргумента. Обе функции "ARM_int_lock_SYS()" и "ARM_int_unlock_SYS()" определены в ассемблерном модуле "arm_exc.s". Ассемблерный код обеих функций идентичен встроенным ассемблерным вставкам, использующимся при компиляции кода под набор инструкций ARM (см. примечания 9-11).

(5-6) Прототипы ассемблерных функций, необходимые для компилятора "Си".
(Примечание: в варианте для C++ проторипы должны определяться с квалификатором компоновки "extern "C"").

(7) При использовании набора инструкций ARM (компилятор gcc вызывается с опцией "-marm"), критические секции могут быть реализованы эффективнее - без избыточных вызовов функций на входе и выходе из критической секции.

(8,11) Макрос "ARM_INT_LOCK()" реализован в виде составной конструкции "do {...} while (0)" для того, чтобы гарантиорвать синтаксическую правильность распознавания макроса в любом контексте (включая случай "висящего else").

(9) GNU gcc поддерживает встроенный ассемблер с "Си"-подобными выражениями в качестве операндов. Данная особенность используется для передачи аргумента ("key_") из "Си" в ассемблерную ирструкцию "MSR", которая сохраняет содержимое "CPSR" в регистре, указанном в качестве аргумента.

(10) Прерывания запрещаются одновременной установкой битов "I[RQ]" и "F[IQ]" регистра "CPSR". Используется наиболее компактный формат инструкции "MSR" (load-immediate form), в котором она не изменяет другие регистры.

Примечание. У инструкции "MSR" есть побочный эффект, переводящий вычислительное ядро ARM в режим "SYSTEM". В данном случае указанный эффект не создаёт никаких проблем, так как весь "Си"-код, включая обработчики IRQ и FIQ, исполняются в режиме "SYSTEM" и никакие другие режимы работы ядра с уровня "Си"-кода не видны (см. шестую часть статьи).

(12) Макрос "ARM_INT_UNLOCK()" восстанавливает содержимое регистра "CPSR" из сохранённого при блокировке аргумента ("key_"). И опять, используется наиболее компактный формат инструкции "MRS" (store-immediate), не меняющий содержимое других регистров.


7.4 Разбор реализации критических секций

Различные статьи и примеры для ARM, доступные в сети (например, [11, 12]) предлагают более трудоёмкие способы блокировки и разрешения прерываний. А именно: используют операцию чтение-модификация-запись регистра "CPSR" вместо загрузки константы.

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

В тот момент, когда активируется вывод IRQ процессора ARM, сбрасывается бит "I" (CPSR[7]), вычислительное ядро завершает выполнение текущей инструкции и начинает обработку прерывания, состоящую из следующих шагов (см. "ARM Architecture Reference Manual, 2nd Edition", раздел 2.6.6 [13]):

  • R14_irq = адрес следующей инструкции прерванного кода плюс четыре
  • SPSR_irq = CPSR
  • CPSR[4:0] = 0b10010 (режим IRQ)
  • CPSR[7] = 1, Примечание: CPSR[6] не меняется
  • PC = 0x00000018

Техническая заметка компании ARM "Что случается, если прерывание возникает в процессе его блокировки ?" [11] указывает на две возможные проблемы: первая проблема относится к специальной функции, используемой как в роли обработчика прерываний, так и в роли обычной функции, вызываемой вне контекста прерываний.

Для выяснения в каком режиме ядра произошёл вызов, такие функции двойного назначения обязаны проверять содержимое регистра "SPSR_irq", что может быть проблематично. Такой сценарий невозможен для описываемой в данной статье методики обработки прерываний, так как "Си"-функция обработчика прерываний вызывается из режима SYSTEM, в котором программист не имеет доступа к регистру "SPSR".

Вторая проблема, описываемая в технической заметке [11] компании ARM, имеет больше отношения к теме обсуждения и относится к ситуации, когда IRQ и FIQ прерывания запрещаются одновременно, актуальной для реализации критических секций (см. листинг 7.1(10) ).

Если IRQ прерывание происходит в момент записи регистра "CPSR", ядро ARM7TDMI устанавливает биты "I" и "F" как в регистре "CPSR", так и в "SPSR_irq" (Saved Program Status Register), после чего входит в обработку прерывания. Если обработчик IRQ не разрешает FIQ явным образом, то быстрые прерывания остаются запрещёнными на время работы обработчика IRQ и далее до выхода из критической секции.

Эта ситуация демонстрирует эффект инверсии приоритета и увеличивает время реакции на FIQ прерывание (латентность) за допустимые пределы. Одним из решений, рекомендованных в технической заметке фирмы ARM, является возможно более раннее явное разрешение FIQ прерываний в обработчике IRQ. Именно это и делает ассемблерная "обёртка" "ARM_irq()", детально обсуждаемая в следующей восьмой части статьи.

Для полноты, следует обсудить статью "Запрещение прерываний на уровне процессора" [12] фирмы Atmel, которая описывает другую возможную проблему, могущую проявиться, когда IRQ или FIQ прерывания возникают непосредственно в момент их запрещения.

Вопрос, поднимаемый в статье [12] возникает, если обработчик IRQ или FIQ прерывания начинает манипулировать битами "I" или "F" регистра "SPSR", что может привести к разрешению прерываний сразу после передачи управления из обработчика в самом начале критической секции.

Указанная ситуация не имеет отношения к описываемой в данной статье методике обработки прерываний, так как обе ассемблерные "оболочки" "ARM_irq()" и "ARM_fiq()" никогда не меняют содержимое регистра "SPSR", что как раз и является первым способом разрешения проблемы, предлагаемым в статье [12].

ПредпросмотрAttachmentSize
bare_metal_arm_systems_html.zip327.73 КБ
blinky_files.zip173.94 КБ