Keil C51: запрещение прерываний в критических секциях программы

Александр Бельченко
7 августа 2003
В статье рассматриваются различные способы запрещения прерываний в критических секциях программы, написанной на языке Си для 8051. Возможно, статья не откроет ничего нового для вас. Я лишь попытался разобраться в механизмах работы разных способов.
В статье использованы следующие материалы:
описание директивы DISABLE из документа C51.pdf «Cx51 Compiler», стр.35 (перевод); описание библиотечной функции «_testbit_» из документа C51lib.chm (перевод).

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

Понятие «критическая секция программы» (critical section) означает, что этот участок программы должен выполняться без прерываний со стороны других процессов (ветвей программы). Поясню эту мысль на простом «жизненном» примере.

При обмене информацией через глобальные переменные может возникнуть ситуация, когда к одной и той же переменной одновременно будут пытаться обратиться две ветви программы — основная и ветвь прерывания. Если обе ветви программы захотят «одновременно» прочитать значение такой переменной, то все пройдет гладко. Но такой вариант развития событий встречается очень редко, гораздо чаще в одной ветви программы значение переменной изменяется, а в другой — читается. Если прерывание одной ветви программы наступит во время обращения к переменной (например, основная программа считывает значение некоторого счетчика, который инкрементирует подпрограмма прерывания), то это приведет к тому, что будут считаны неверные данные. Строго говоря, это верно лишь для многобайтных переменных (типа int, long или float). Обращение к переменным типа char и bit происходит за одну инструкцию процессора, поэтому работа с ними всегда будет происходить корректно.

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

    EA = 1; /* разрешаем обработку всех немаскированных прерываний */

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

Несколько способов защиты критических секций программы

Самым простым и очевидным способом защиты критических секций может считаться следующий: при входе в критическую секцию запретить прерывания инструкцией EA = 0; , при выходе из критической секции необходимо восстановить бит разрешения прерываний инструкцией EA = 1;. Этот метод хорошо подходит для реализации коротких критических секций в теле одной функции. Если же внутри критической секции выполняется вызов других функций, то программисту следует быть внимательным, чтобы не совершить одну трудно улавливаемую ошибку: если в вызываемой функции также происходит вход в критическую секцию, то при возврате из такой функции бит EA будет установлен в «1» и программа «незаметно» покинет критическую секцию.

Для того, чтобы облегчить работу программиста с критическими секциями, создатели компилятора Си фирмы Keil Software ввели директиву компилятора DISABLE, которая позволяет автоматически генерировать специальный код при входе в критическую секцию и выходе из нее. Причем автоматически поддерживается «рекуррентный» вход в критическую секцию программы — при входе в каждую критическую секцию сохраняется текущее значение бита EA, а при выходе из критической секции значение бита EA восстанавливается в прежнее состояние. Прочитать о директиве DISABLE вы можете в документе C51.pdf на странице 35 (мой перевод).

Предлагаю вам вместе со мной разобраться как работают разные способы защиты критических секций, чтобы узнать их достоинства и недостатки. Вы можете загрузить пример программы critical, в которой использованы несколько способов запрещения прерываний в критических секциях программы. В этой программе имеется глобальная переменная unsigned long counter, которая используется для обмена информацией между основной ветвью программы и ветвью прерывания от таймера 0 (функция void timer0() interrupt 1 using 1). При каждом прерывании таймера 0 значение переменной counter инкрементируется. Ветвь основной программы некоторым образом анализирует значение этой переменной. Для доступа к переменной counter из основной ветви программы используются три функции get_long1, get_long2, get_long3. Все три функции представляют собой критический участок программы — они возвращают текущее значение переменной counter, но в каждой функции используется свой способ запрещения прерываний.

Использование директивы DISABLE иллюстрирует функция get_long1:

#pragma DISABLE

unsigned long get_long1()
{
    return counter;
}

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

; FUNCTION get_long1 (BEGIN)
          SETB    C
          JBC     EA,?C0013
          CLR     C
?C0013:
          PUSH    PSW

Состояние бита EA сохранятеся во флаге Carry, а затем регистр PSW (который содержит флаг Carry) сохраняется на стеке. Команда JBC EA,?C0013 анализирует и одновременно очищает бит EA.

Восстановление значения EA происходит в обратном порядке. Этот кусок кода также добавлен благодаря директиве DISABLE:

?C0002:
          POP     PSW
          MOV     EA,C
          RET
; FUNCTION get_long1 (END)

Механизм действия директивы DISABLE достаточно прост и понятен: при входе и выходе в функцию добавляется специальный код, который сохраняет и восстанавливает значение EA. В теле самой функции прерывания запрещены.

В примечании к описанию директивы DISABLE сказано, что функция, на которую распространяется действие этой директивы, не может возвратить значение типа bit. Если посмотреть на вышеприведенный ассемблерный листинг функции get_long1, то становится понятно почему — для возвращения значения типа bit в компиляторе С51 используется бит Carry. При использовании же директивы DISABLE этот бит активно используется при манипуляциях с битом EA. Поэтому при попытке вернуть битовое значение из такой функции вы получите на выходе значение бита EA вместо результата работы функции. Это ограничение не столь существенно для применения в рамках доступа к многобайтным данным в критических секциях программы.

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

Функция get_long2 иллюстрирует метод ручного манипулирования с битом EA. Для хранения значения бита EA используется локальная переменная bit EA_store. Вход в критическую секцию производится следующим образом:

    // сохраняем значение EA и запрещаем прерывания
    EA_store = EA;
    EA       = 0;

При выходе из критической секции восстанавливаем значение EA:

    // восстанавливаем EA
    EA       = EA_store;

Вот так — все просто и понятно. Остается вопрос — насколько этот прием эффективен? Сравнение работы функций get_long1 и get_long2 показывает, что вторая выполняется несколько быстрее. Но меня заинтересовал метод анализа значения бита EA, реализованный в компиляторе для директивы DISABLE. А именно — использование команды ассемблера JBC EA, label. «Что-то в этом подходе есть, какая-то глубокая мысль», — подумал я. А потому реализовал третий вариант защиты критических секций, который является своеобразным соединением первого и второго метода.

В функции get_long3 использован еще один метод ручной манипуляции с битом разрешения прерываний EA. При входе в критическую секцию значение бита EA анализируется при помощи ассемблерной инструкции JBC EA, label и сохраняется в локальной переменной bit EA_store. В наборе библиотечных функций компилятора C51 от Keil µVision2 есть функция «_testbit_», при использовании которой генерируется ассемблерная инструкция JBC. Подробнее о функции «_testbit_» можно прочитать в файле справки C51lib.chm или же можете ознакомиться с моим переводом.

Вход в критическую секцию в функции get_long3 производится следующим образом:

    // сохраняем значение EA и запрещаем прерывания
    EA_store = 1;
    if ( !_testbit_(EA) )   EA_store = 0;

При выходе из критической секции значение EA восстанавливаем следующим способом:

    // восстанавливаем EA
    if ( EA_store )         EA = 1;

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

; Вход в критическую секцию
     SETB    EA_store
     JBC     EA,?C0004
     CLR     EA_store
?C0004:

; Выход из критической секции
     JNB     EA_store,?C0005
     SETB    EA
?C0005:

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

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

Метод защитыИмя функции в примереХранение значения EAВремя выполнения защитного кода в тактах
#pragma DISABLEget_long1На стеке9 (10 если EA=0)
Ручное сохранение EAget_long2Локальная битовая переменная7
Сохранение EA c использованием _testbit_get_long3Локальная битовая переменная6

Как видно из этой таблицы первый метод (от Keil µVision2) работает медленнее всего, к тому же время выполнения кода зависит от значения бита EA. В третьем методе, который придумал я, время выполнения защитного кода выравнивается за счет конструкции if ( EA_store ) EA = 1;.

К достоинствам первого метода с директивой DISABLE можно отнести то, что значение EA хранится не в битовой переменной, а на стеке (учитывайте это при определении необходимого размера стека); и еще — необходимый защитный код генерируется компилятором автоматически и поэтому исходный Си-код меньше по размеру. Главным достоинством моего метода (с использованием функции _testbit_) является скорость выполнения защитного кода. Но самым-самым быстрым методом будет использование пары команд: EA=0 и EA=1.