Под атомарным доступом будем понимать такое обращение (операции чтения, модификации, записи, или их последовательность) к переменной или периферийному регистру, которое не приведет к конфликту при возникновении прерывания или приоритетного вытеснения задачи в 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
__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
, которая обеспечивает атомарный доступ в любом случае, даже если требуется изменить большое битовое поле.
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/.
Я не являюсь автором данных макросов.
BFAR()
BFAM()
, BFAMI()
, BFAMD()
BFAR()
, но биты, на которые влияет операция увазываются не диапазоном, а маской. Полное описание будет чуть позже
BFAR()
-mlarge-scalar
(разрешение располагать скалярные переменные в far области ОЗУ) выдавалась ошибка при использовании макроса BFAR()
с произвольной переменной (без атрибута SFR).BFA()
BFA()
выдавалось предупреждение о безусловном преобразовании большой константы в unsigned int
BFARI()
BFARI()
обеспечивает доступ к структуре по указателюBFA_SET
и BFA_CLR
BFA_IV
заменено на BFA_INV
. Операция инвертирования теперь инвертирует биты по передаваемой маске.
Архив содержит заголовочный файл bfa.h
, который включает в себя четыре макроса, реализующих атомарный доступ к полю структуры или к любой скалярной переменной. Один из макросов предназначен для работы со структурами, именование которых соответствует правилам Microchip C30 для периферийных регистров. Остальный макросы могут использоваться с любой структурой или переменной размером не больше машинного слова (int
).
Для использования макросов необходимо и достаточно подключить к модулю файл bfa.h
и включить оптимизацию не ниже -01
.
Список макросов
BFA(comm, reg_name, field_name, …)
- Bit Field AccessBFAR(comm, reg_name, lower, upper, …)
- Bit Field Access using RangeBFARI(comm, pt, lower, upper, …)
- Bit Field Access using Range IndirectBFARD(comm, val, lower, upper, …)
- Bit Field Access using Range Direct
Макрос обеспечивает атомарный доступ к именованым полям структур периферийных регистров микроконтроллеров 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
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 */
Макрос обеспечивает атомарный доступ к битовому полю любой структуры или переменной, которая находится в области 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
upper
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 */
Макрос обеспечивает атомарный доступ к битовому полю любой структуры или переменной по указателю. Битовое поле указывается в виде диапазона (младщий бит / старший бит). Переменная может находиться как в NEAR, так и в FAR DATA SPACE.
Вызов:
BFARI(comm, pt, lower, upper, ...)
Параметры:
comm
BFA_WR |
запись в битовое поле |
BFA_RD |
чтение битового поля |
BFA_SET |
установка битов в битовом поле по маске |
BFA_CLR |
сброс битов в битовом поле по маске |
BFA_INV |
инвертирование битов в битовом поле по маске |
pt
int*
.lower
upper
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 */
Макрос обеспечивает атомарный доступ к битовому полю любой структуры или переменной, которая находится в любой области ОЗУ (NEAR или FAR). Битовое поле указывается в виде диапазона (младщий бит / старший бит).
Вызов:
BFARD(comm, val, lower, upper, ...)
Параметры:
comm
BFA_WR |
запись в битовое поле |
BFA_RD |
чтение битового поля |
BFA_SET |
установка битов в битовом поле по маске |
BFA_CLR |
сброс битов в битовом поле по маске |
BFA_INV |
инвертирование битов в битовом поле по маске |
val
PORTA
, TRISB
, CMCON
и т.п. Может быть любой скалярной переменной программы.lower
upper
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);
Последнее означает, что должно выполняться одно из двух условий:
xor
с прямым доступом к интересующей области памяти
К основным достоинствам макросов 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