Содержание

Атомарный доступ к структурам

Проблема атомарного доступа

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

Простой пример: необходимо инвертировать вывод RA0 контроллера:

_LATA0 = ~_LATA0;

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

10:                        _LATA0 = ~_LATA0;
  0288  202C41     mov.w #0x2c4,0x0002
  028A  784091     mov.b [0x0002],0x0002
  028C  60C0E1     and.b 0x0002,#1,0x0002
  028E  A20401     btg 0x0002,#0
  0290  BFC2C4     mov.b 0x02c4,0x0000
  0292  A10400     bclr 0x0000,#0
  0294  704001     ior.b 0x0000,0x0002,0x0000
  0296  B7E2C4     mov.b 0x0000,0x02c4

Никогда так не делайте! C30 предоставляет встроенную функцию __builtin_btg(), которая атомарно инвертирует бит в адресном пространстве. Код выше - пример некорректного обращения к периферии.

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

  0288  202C41     mov.w #0x2c4,0x0002

возникло прерывание, которое так же обращается к регистру LATA, модифицирует его, например, устанавливает какой-нибудь бит. После выхода из обработчика прерывания начнет выполняться инструкция по адресу 0x028A. Но ведь значение значение LATA, которое было до прерывания уже сохранено! И модифицироваться будет именно оно, а не актуальное состояние защелки порта. И после выполнения

  0296  B7E2C4     mov.b 0x0000,0x02c4

в порт запишется значение, которое не учитывает установку бита в прерывании. Графитовый стержень не опустился. Бум!

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

Проблема становится еще более актуальной при работе с архитектурами типа Read-Modify-Write (ARM, MIPS) которые не имеют инструкций прямой модификации памяти в адресном пространстве. Для изменения значения периферийного регистра или переменной требуется загрузка данных в регистр АЛУ, модификации и выгрузка обратно. А ведь есть еще многоядерные процессоры…

Методы решения

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

Запрещение прерываний

DI();
_LATA0 = ~_LATA0;
EI();

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

Если целевая платформа включает в себя гибкий контроллер прерывания, то побочные эффекты такого метода могут быть довольно сильными, так как необходимо выполнить больше действий для запрещения прерываний. Например, для PIC24/dsPIC нужно сохранить во временной переменной (в стеке) текущий приоритет ядра, установить приоритет ядра на максимум, а после выполнения атомарной операции доступа восстановить приоритет из временной переменной. Это является единственно корректным способом запрещения прерываний при разработке ПО на языке Си (использование инструкции disi не рекомендуется).

Критическая секция

tn_sys_enter_critical();
_LATA0 = ~_LATA0;
tn_sys_exit_critical();

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

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

Мютексы

tn_mutex_lock(&mutex, TIMEOUT);
_LATA0 = ~_LATA0;
tn_mutex_unlock(&mutex);

Мютекс - это объект RTOS, предназначенный для реализации конкурентного доступа к общему для задач ресурсу.

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

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

Ограничивать мютексом рекомендуется относительно большую часть кода, но никак не доступ к полю структуры или биту порта.

Аппаратные методы

Некоторые архитектуры имеют удобные способы обеспечения атомарного доступа. Из известных мне это Cortex-M3 с его "bit-band" областью в адресном пространстве и MIPS32 с инструкциями LL и SC. Последние, впрочем, предназначены для использования в многоядерных процессорах, однако так же успешно могут применяться и при работе с PIC32.

К аппаратным методам обеспечения атомарного доступа можно так же отнести инструкцию disi 16-битных контроллеров Microchip. Эта инструкция запрещает прерывания на определенное количество командных тактов.

Доступ к битовым полям структуры

Мы рассмотрели общую проблему конкурентного доступа к ресурсам программы. Однако, существует и частный случай этой проблемы - доступ к битовым полям структуры.

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

__extension__ typedef struct tagOC4CONBITS {
  union {
    struct {
      unsigned OCM    :3;
      unsigned OCTSEL :1;
      unsigned OCFLT  :1;
      unsigned        :8;
      unsigned OCSIDL :1;
    };
    struct {
      unsigned OCM0   :1;
      unsigned OCM1   :1;
      unsigned OCM2   :1;
    };
  };
} OC4CONBITS;
extern volatile OC4CONBITS OC4CONbits __attribute__((__sfr__));

При доступе к этим битовым полям компилятор может сгенерировать атомарную инструкцию:

75:                        AD1CON1bits.ADON = 1;
 00298  A8E321     bset.b 0x0321,#7
76:                        AD1CON1bits.ADON = 0;
 0029A  A9E321     bclr.b 0x0321,#7

Но все станет гораздо хуже, когда вы попытаетесь записать в поле структуры значение переменной:

74:                        a = 1;
 00298  200010     mov.w #0x1,0x0000
 0029A  884010     mov.w 0x0000,0x0802
75:                        b = 0;
 0029C  EF2800     clr.w 0x0800
76:
77:                        AD1CON1bits.ADON = a;
 0029E  804011     mov.w 0x0802,0x0002
 002A0  DD08C7     sl 0x0002,#7,0x0002
 002A2  BFC321     mov.b 0x0321,0x0000
 002A4  A17400     bclr 0x0000,#7
 002A6  704001     ior.b 0x0000,0x0002,0x0000
 002A8  B7E321     mov.b 0x0000,0x0321
78:                        AD1CON1bits.ADON = b;
 002AA  804001     mov.w 0x0800,0x0002
 002AC  DD08C7     sl 0x0002,#7,0x0002
 002AE  BFC321     mov.b 0x0321,0x0000
 002B0  A17400     bclr 0x0000,#7
 002B2  704001     ior.b 0x0000,0x0002,0x0000
 002B4  B7E321     mov.b 0x0000,0x0321

Проблемы так же могут появиться при доступе к битовому полю, размер которого больше 1 бита:

82:                        IPC0bits.INT0IP = 2;
 002BE  BFC0A4     mov.b 0x00a4,0x0000
 002C0  B3CF81     mov.b #0xf8,0x0002
 002C2  604001     and.b 0x0000,0x0002,0x0000
 002C4  A01400     bset 0x0000,#1
 002C6  B7E0A4     mov.b 0x0000,0x00a4

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

Рассмотрим пример: запись в младшие четыре бита регистра TRISB значения 0x000A.

 002FA  801630     mov.w TRISB, W0
 002FC  68006A     xor.w W0, #0x000A, W0
 002FE  60006F     and.w W0, #0x000F, W0
 00300  B6A2C6     xor.w TRISB
  • TRISB = 0xFFFF
  • первая инструкция загружает значение TRISB в регистр W0;
  • вторая инструкция атомарно и безопасно модифицирует четыре младших бита регистра W0;
  • третья инструкция накладывает на регистр W0 маску, выделяя 4 младших бита
  • последняя инструкция атомарно и безопасно (не трогая остальных битов!) модифицирует TRISB; TRISB = 0xFFFA;

Даже если после выполнения первой инструкции возникнет прерывание, которое изменит значение TRISB (естественно, не младших 4-х битов), операция выполниться корректно. Приведенный выше код является атомарным.

О "волшебном" свойстве XOR я знал давно, но формализованный подход увидел первый раз на форуме microchip.com. Автор замечательной коммерческой RTOS AVIX-RT выложил макрос, который использует inline ассемблер для реализации xor доступа. Затем в состав AVIX вошел заголовочный файл, в котором реализованы расширенные варианты подобного макроса для архитектур PIC24/dsPIC (компилятор C30) и PIC32 (MIPS32 M4K, компилятор C32).

По моему скромному мнению эти макросы являются высшим пилотажем. В них используется много интересных решений которые я рассмотрю ниже.

Макросы атомарного доступа к полям структуры

Согласно лицензионному соглашению, код, который содержится в файле AVIXAtomicSFR.h является неотъемлемой частью RTOS AVIX-RT и не может быть приведен полностью или частично без согласования с держателем права. Поэтому пришлось сделать небольшой рефакторинг. Оригинальный код входит в состав AVIX-RT, демонстрационная версия которой может быть скачана с сайта http://www.avix-rt.com/.

Я не являюсь автором данных макросов.

Скачать










Описание

Архив содержит заголовочный файл bfa.h, который включает в себя четыре макроса, реализующих атомарный доступ к полю структуры или к любой скалярной переменной. Один из макросов предназначен для работы со структурами, именование которых соответствует правилам Microchip C30 для периферийных регистров. Остальный макросы могут использоваться с любой структурой или переменной размером не больше машинного слова (int).

Для использования макросов необходимо и достаточно подключить к модулю файл bfa.h и включить оптимизацию не ниже -01.

Список макросов


BFA()



Макрос обеспечивает атомарный доступ к именованым полям структур периферийных регистров микроконтроллеров PIC24/dsPIC.

Вызов:

BFA(comm, reg_name, field_name, ...)

Параметры:

comm
тип доступа:

BFA_WR запись в битовое поле
BFA_RD чтение битового поля
BFA_SET установка битов в битовом поле по маске
BFA_CLR сброс битов в битовом поле по маске
BFA_INV инвертирование битов в битовом поле по маске

reg_name
имя периферийного регистра, к которому осуществляется доступ, например, PORTA, TRISB, CMCON и т.п.
field_name
имя битового поля в структуре регистра (см. документацию на микроконтроллер и заголовочный файл C30 для этого микроконтроллера). Например, для регистра IPC0 это может быть T1IP.
. . .
необязательный параметр, используется только если comm = [BFA_WR, BFA_SET, BFA_CLR, BFA_INV]. Если comm = BFA_RD - параметр не указывается. Если comm = BFA_WR, параметр указывает значение, которое записывается в битовое поле. В остальных случаях параметр должен быть равен битовой маске. Параметр может быть переменной.

Пример вызова:

BFA(BFA_WR,  IPC0, INT0IP, 0);         /* Запись в поле INT0IP регистра IPC0
                                          константы 0 */
BFA(BFA_INV, IPC0, INT0IP, (1 << 0));  /* Инвертирование младшего бита поля
                                          INT0IP регистра IPC0 */
BFA(BFA_WR,  IPC0, INT0IP, 4);         /* Запись в поле INT0IP регистра IPC0
                                          константы 4 */
BFA(BFA_CLR, IPC0, INT0IP, (1 << 0));  /* Сброс младшего бита поля INT0IP
                                          регистра IPC0 */
a = 2;
BFA(BFA_WR,  IPC0, INT0IP, a);  /* Запись в поле INT0IP регистра IPC0 значения
                                   переменной a */
b = BFA(BFA_RD, IPC0, INT0IP);  /* Чтение значения поля INT0IP регистра IPC0
                                   в переменную b */


BFAR()



Макрос обеспечивает атомарный доступ к битовому полю любой структуры или переменной, которая находится в области NEAR DATA SPACE (первые 8 кБ ОЗУ). Битовое поле указывается в виде диапазона (младщий бит / старший бит).

Вызов:

BFAR(comm, reg_name, lower, upper, ...)

Параметры:

comm
тип доступа:

BFA_WR запись в битовое поле
BFA_RD чтение битового поля
BFA_SET установка битов в битовом поле по маске
BFA_CLR сброс битов в битовом поле по маске
BFA_INV инвертирование битов в битовом поле по маске

reg_name
имя регистра, к которому осуществляется доступ, например, PORTA, TRISB, CMCON и т.п. Может быть любой переменной программы, которая находится в области near памяти данных (первые 8 кБ)
lower
младший бит поля структуры, от 0 до 15
upper
старший бит поля структуры, от 0 до 15, upper >= lower
. . .
необязательный параметр, используется только если comm = [BFA_WR, BFA_SET, BFA_CLR, BFA_INV]. Если comm = BFA_RD - параметр не указывается. Если comm = BFA_WR, параметр указывает значение, которое записывается в битовое поле. В остальных случаях параметр должен быть равен битовой маске. Параметр может быть переменной.

Пример вызова:

BFAR(BFA_WR,  TRISB, 0,  7, 0xAA);    /* Запись 0xAA в младший байт регистра
                                         TRISB    */
BFAR(BFA_INV, TRISB, 8, 15, 0xFFFF);  /* Инвертирование старшего байта
                                         регистра TRISB */
a = 2;
BFAR(BFA_SET, TRISB, 0,  7, a);       /* Установка битов в младшем байте
                                         TRISB по маске в переменной a */
b = BFAR(BFA_RD, TRISB, 0,  7);       /* Чтение значения младшего байта
                                        регистра TRISB в переменную b */


BFARI()



Макрос обеспечивает атомарный доступ к битовому полю любой структуры или переменной по указателю. Битовое поле указывается в виде диапазона (младщий бит / старший бит). Переменная может находиться как в NEAR, так и в FAR DATA SPACE.

Вызов:

BFARI(comm, pt, lower, upper, ...)

Параметры:

comm
тип доступа:

BFA_WR запись в битовое поле
BFA_RD чтение битового поля
BFA_SET установка битов в битовом поле по маске
BFA_CLR сброс битов в битовом поле по маске
BFA_INV инвертирование битов в битовом поле по маске

pt
указатель на периферийный регистр или любую переменную. Макрос автоматически приводит указатель к типу int*.
lower
младший бит поля структуры, от 0 до 15
upper
старший бит поля структуры, от 0 до 15, upper >= lower
. . .
необязательный параметр, используется только если comm = [BFA_WR, BFA_SET, BFA_CLR, BFA_INV]. Если comm = BFA_RD - параметр не указывается. Если comm = BFA_WR, параметр указывает значение, которое записывается в битовое поле. В остальных случаях параметр должен быть равен битовой маске. Параметр может быть переменной.

Пример вызова:

BFAR(BFA_WR,  &TRISB, 0,  7, 0xAA);    /* Запись 0xAA в младший байт регистра
                                          TRISB    */
BFAR(BFA_INV, &TRISB, 8, 15, 0xFFFF);  /* Инвертирование старшего байта
                                         регистра TRISB */
a = 2;
BFAR(BFA_SET, &TRISB, 0,  7, a);       /* Установка битов в младшем байте
                                          TRISB по маске в переменной a */
c = &TRISD;
b = BFAR(BFA_RD, c, 0,  7);            /* Чтение значения младшего байта
                                          регистра TRISD в переменную b */


BFARD()



Макрос обеспечивает атомарный доступ к битовому полю любой структуры или переменной, которая находится в любой области ОЗУ (NEAR или FAR). Битовое поле указывается в виде диапазона (младщий бит / старший бит).

Вызов:

BFARD(comm, val, lower, upper, ...)

Параметры:

comm
тип доступа:

BFA_WR запись в битовое поле
BFA_RD чтение битового поля
BFA_SET установка битов в битовом поле по маске
BFA_CLR сброс битов в битовом поле по маске
BFA_INV инвертирование битов в битовом поле по маске

val
имя регистра, к которому осуществляется доступ, например, PORTA, TRISB, CMCON и т.п. Может быть любой скалярной переменной программы.
lower
младший бит поля структуры, от 0 до 15
upper
старший бит поля структуры, от 0 до 15, upper >= lower
. . .
необязательный параметр, используется только если comm = [BFA_WR, BFA_SET, BFA_CLR, BFA_INV]. Если comm = BFA_RD - параметр не указывается. Если comm = BFA_WR, параметр указывает значение, которое записывается в битовое поле. В остальных случаях параметр должен быть равен битовой маске. Параметр может быть переменной.

Пример вызова:

BFARD(BFA_WR,  qwer, 0,  7, 0xAA);     /* Запись 0xAA в младший байт
                                          переменной qwer */
BFARD(BFA_INV, TRISB, 8, 15, 0xFFFF);  /* Инвертирование старшего байта
                                         регистра TRISB */
a = 2;
BFARD(BFA_SET, TRISB, 0,  7, a);       /* Установка битов в младшем байте
                                         TRISB по маске в переменной a */
b = BFARD(BFA_RD, TRISB, 0,  7);       /* Чтение значения младшего байта
                                        регистра TRISB в переменную b */


Пояснения

Проверка параметров

Приведенные выше макросы проверяют передаваемый параметр comm, указывающий на метод доступа к полю структуры. Остальные параметры не проверяются. Для comm определены следующие разрешенные значения:

#define BFA_WR    0xAAAA        /* Запись в битовое поле                  */
#define BFA_RD    0x5555        /* Чтение битового поля                   */
#define BFA_IV    0x9999        /* Побитовое инвертирование битового поля */

Контроль передачи параметров в макрос сделан интересным способом: в заголовочном файле объявлены уникальные для каждого параметра типы:

#define __BFA_COMM_ERR(a)               __BFA_COMMAND_ERROR_##a
#define __BFA_COMM_GET(a)               __BFA_COMM_ERR(a)
 
typedef int __BFA_COMM_GET(BFA_WR);
typedef int __BFA_COMM_GET(BFA_RD);
typedef int __BFA_COMM_GET(BFA_IV);

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

__BFA_COMM_GET(comm) v = __VA_ARGS__+0;

Если передаваемый в макрос параметр отличается от определенных BFA_WR, BFA_RD и BFA_IV, то компилятор выдаст ошибку:

source\appl\appl.c: In function 'main':
source\appl\appl.c:73: error: '__BFA_COMMAND_ERROR_0' undeclared (first use in this function)

Условный оператор ? :

Использование условного оператора ?: позволило реализовать с помощью одного макроса как выполнение операции, так и возврат значения. Если не вдаваться в детали реализации, все макросы выглядят следующим образом:

#define BFAXX(comm, ...)
({
    ((comm) == BFA_WR)
    ? ({
      })
      /* ........ */
    : ({ /* BFA_RD */
         __BFA_STRUCT_VAL(reg_name).field_name;
      })
})

Если параметр comm (тип доступа к структуре) равен BFA_RD, то макрос генерирует следующее выражение:

__BFA_STRUCT_VAL(reg_name).field_name;

где

#define __BFA_STRUCT_VAL(a)   a##bits

Таким образом, написав

b = BFA(BFA_RD, IPC0, INT0IP);

после работы препроцессора получим простое присваивание:

b = IPC0bits.INT0IP;

Примеры использования

В оригинальной документации, которая поставляется с демонстрационной версией AVIX-RT приведено много примеров использования этих макросов. К сожалению, лицензионное соглашение запрещает публикацию документа третьим лицам. Однако никто не запрещает зарегистрироваться на сайте http://www.avix-rt.com/ и скачать AVIX-RT.

Конечно, прямое использование макросов BFA() и BFAR() не слишком наглядно. Тем не менее это один из методов:

BFA(BFA_WR, IPC0, INT0IP, 0);    /* Запись в поле INT0IP регистра IPC0 константы 0 */
BFA(BFA_WR, IPC0, INT0IP, 4);    /* Запись в поле INT0IP регистра IPC0 константы 4 */

Если регистр используется в вашем приложении только на запись, можно определить следующий макрос:

#define LED(v)   BFA(BFA_WR, LATB, LATB0, (v))

и затем использовать уже его:

LED(1);
LED(0);

Если поле структуры используется как на запись, так и на чтение, можно определить следующий макрос с переменным количеством параметров:

#define TMR1_PS(a, ...)   BFA((a), T1CON, TCKPS, __VA_ARGS__)

Использовать это макрос нужно так:

TMR1_PS(BFA_WR, 0);
a = TMR1_PS(BFA_RD);

Очень часто необходимо получить атомарный доступ к необъявленному полю структуры - например, установить определенное значение на нескольких выводах контроллера. Для этого можно использовать макрос BFAR():

#define LCD_PORT(v)  BFAR(BFA_WR, LATE,  0, 3, (v))

А дальше очевидно:

LCD_PORT(0x02);
LCD_PORT(0x00);

Выводы

Использование подобных макросов обязательно для всех, кто хочет писать быстрый и безопасный код для микроконтроллеров PIC24/dsPIC с использованием компилятора C30.

Рассмотренный подход может быть реализован и для любой другой архитектуры и другого компилятора. Для этого необходимо чтобы:

  • компилятор имел гибкий inline-ассемблера (например, как у gcc)
  • архитектура имела механизмы прямого доступа к памяти

Последнее означает, что должно выполняться одно из двух условий:

  • Набор инструкций должен включать в себя инструкцию xor с прямым доступом к интересующей области памяти
  • Архитектура должна иметь специальные механизмы прямого доступа. Примером может служить bit-band область у Cortex-M3 или регистры SET/CLR/INV для всей периферии у PIC32.

К основным достоинствам макросов BFA() и BFAR() относится реализация атомарного доступа к разделяемым ресурсам. Конечно, это не означает, что в прерывании и в основном коде можно безболезненно шевелить одной и той же ногой контроллера. Но доступ к разным битам порта осуществляется безопасно.

При использовании макросов BFA() и BFAR() нет необходимости запрещать прерывания или реализовывать критическую секцию. Это позволяет детерминировать время входа в прерывание, уменьшить объем занимаемого кода и скорость выполнения операций.

Более того при использовании макросов доступа к полям структуры уменьшается объем кода и время выполнение по сравнению с непосредственным доступом:

75:                        IPC0bits.INT0IP = 4;
 00294  BFC0A4     mov.b 0x00a4,0x0000
 00296  B3CF81     mov.b #0xf8,0x0002
 00298  604001     and.b 0x0000,0x0002,0x0000
 0029A  A02400     bset 0x0000,#2
 0029C  B7E0A4     mov.b 0x0000,0x00a4
76:                        BFA(BFA_WR, IPC0, INT0IP, 4);
 0029E  800520     mov.w 0x00a4,0x0000
 002A0  A22000     btg 0x0000,#2
 002A2  600002     and.w 0x0000,0x0004,0x0000
 002A4  B6A0A4     xor.w 0x00a4