Атомарный доступ к битам регистров bit-band через макрос

Не буду расписывать, зачем нужен этот самый атомарный доступ. Будем считать, что если он Вам понадобился, значит надо.
Дело было так. Когда этот самый атомарный доступ понадобился мне, вспомнилось про наличие bit-band региона памяти в STM32. В даташите на Cortex M3 есть формула, по ней можно всё рассчитать, есть рисунки, в общем всё ясно и понятно. Однако хотелось некой автоматизации. Поискал. Нашел ARMовскую infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka4203.html рекомендацию по применению. Уже лучше. Но тут заглянул в хедеры CMSIS и понял, что ARMовские макросы можно «допилить»…

Все базовые значения уже оказались определены, заменил их на «стандартные» из CMSIS. Плюс, для удобства, дописал свой макрос, для обращения к битам по аналогии обращения к портам в CMSIS:
#include "stm32f1xx.h"

#define BITBAND_SRAM(a,b) ((SRAM_BB_BASE + (a-SRAM_BASE)*32 + (b*4)))  // Convert SRAM address
#define BITBAND_PERI(a,b) ((PERIPH_BB_BASE + (a-PERIPH_BASE)*32 + (b*4)))  // Convert PERI address
#define MAILBOX   0x20004000
#define MBX_B0    *((volatile uint32_t *) (BITBAND_SRAM(MAILBOX,0))) // Bit 0
#define MBX_B7    *((volatile uint32_t *) (BITBAND_SRAM(MAILBOX,7))) // Bit 7
#define BB_PERI(c,d) *((volatile uint32_t *) ((PERIPH_BB_BASE + (uint32_t)( &( c ) - PERIPH_BASE)*32 + ( d*4 ))))

int bitbang (void);

int bitbang (void)
{
	uint32_t temp = 0;
    BB_PERI (GPIOB->CRL, 5 ) = 1;        // Бит 5 регистра CRL GPIOB
    temp = BB_PERI (GPIOC->CRL, 0 );     // Бит 0 регистра CRL GPIOC
    temp = MBX_B7;
    BB_PERI (GPIOB->BRR, 0x0E ) = temp;  // Бит 14 регистра BRR GPIOB
    return MBX_B0;
}


Из примера должно быть всё понятно. Макросы для атомарного доступа к ОЗУ не дописывал, т.к. мне пока не надо, их не трудно написать по аналогии с периферией.

GCC Выдал мне такой код:


  28              		.cfi_startproc
  29              		@ args = 0, pretend = 0, frame = 0
  30              		@ frame_needed = 0, uses_anonymous_args = 0
  31              		@ link register save eliminated.
  32              	.LVL0:
  15:../bit-bang.c **** 	uint32_t temp = 0;
  16:../bit-bang.c ****     BB_PERI (GPIOB->CRL, 5 ) = 1;
  33              		.loc 1 16 0
  34 0000 054B     		ldr	r3, .L2
  35 0002 0122     		movs	r2, #1
  36 0004 1A60     		str	r2, [r3]
  17:../bit-bang.c ****     temp = BB_PERI (GPIOC->CRL, 0 );
  37              		.loc 1 17 0
  38 0006 054B     		ldr	r3, .L2+4
  39 0008 1B68     		ldr	r3, [r3]
  40              	.LVL1:
  18:../bit-bang.c ****     temp = MBX_B7;
  41              		.loc 1 18 0
  42 000a 054B     		ldr	r3, .L2+8
  43 000c 1A68     		ldr	r2, [r3]
  44              	.LVL2:
  19:../bit-bang.c ****     BB_PERI (GPIOB->BRR, 0x0E ) = temp;
  45              		.loc 1 19 0
  46 000e 054B     		ldr	r3, .L2+12
  47 0010 1A60     		str	r2, [r3]
  20:../bit-bang.c ****     return MBX_B0;
  48              		.loc 1 20 0
  49 0012 054B     		ldr	r3, .L2+16
  50 0014 1868     		ldr	r0, [r3]
  21:../bit-bang.c **** }
  51              		.loc 1 21 0
  52 0016 7047     		bx	lr
  53              	.L3:
  54              		.align	2
  55              	.L2:
  56 0018 14802142 		.word	1109491732
  57 001c 00002242 		.word	1109524480
  58 0020 1C000822 		.word	570949660
  59 0024 B8822142 		.word	1109492408
  60 0028 00000822 		.word	570949632
  61              		.cfi_endproc


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

С вниманием прислушаюсь к критике, т.к. маленько «плаваю» в указателях и мог написать что-либо не по-феншую.

Комментарии (56)

RSS свернуть / развернуть
Ну, я к битам периферии обращаюсь примерно так:
#define ALIAS_PTR(VarPtr,BitNum) \
*((__IO uint32_t *) (((uint32_t) &(VarPtr) & 0xf0000000UL) | 0x02000000UL \
+ (((uint32_t) &(VarPtr) & 0xfffffUL) << 5) | (BitNum << 2)))

// Применение:
ALIAS_PTR(GPIOC->ODR,8) = 1;
ALIAS_PTR(GPIOC->ODR,8) ^= 1;
ALIAS_PTR(GPIOC->ODR,8) = ~ALIAS_PTR(GPIOC->ODR,8);
0
ALIAS_PTR(GPIOC->ODR,8) ^= 1;
ALIAS_PTR(GPIOC->ODR,8) = ~ALIAS_PTR(GPIOC->ODR,8);
чтение — модификация — запись, не атомарно, т.к. что-либо может «влезть» в этот момент…
А если это был бит состояния какого-то устройства, и за «время отсутствия» он поменялся…
0
То, что прерывание влезет и ОН может поменяться не есть опасная неатомарность. Ведь ОН может поменяться прерыванием сразу после Вашего атомарного доступа. «Неатомарно» — это когда Ваше изменение может испортить ЧУЖОЙ бит. Так что данный способ признается сообществом как «атомарный»))
0
_IO это просто volatile.
0
В принципе — то же самое, только записано по другому. Искал готовое — не нашел, изобретал велосипед как мог — получилось как всегда…
0
Старался сделать запись наиболее понятной, тем более считать будет не МК, а компилятор.
0
Наверное уже пора написать статью, что байт состоит из 8 бит?
0
  • avatar
  • x893
  • 26 марта 2015, 02:20
Что не так?
0
байт состоит из 8 бит
Вообще говоря, нет.
0
Но сегодня-то он все-таки из 8 бит.
0
Ты забыл добавить «в большинстве случаев».
0
Правильнее было бы «если не указана иная размерность»
0
Это уже зависит от контекста…
0
Можно смело писать статью, что октет состоит из 8 бит :)
+1
А байт равен октету, если не оговорен иной размер.
0
Октет — да, но байт и октет — несколько разные вещи…
0
Опять макросы. Да вы с дубу рухнули
BB_PERI (GPIOB->BRR, 88 )
BB_PERI (GPIOB->BRR, «jhjh»)
BB_PERI (GPIOB->BRR, GPIOB->BRR)
0
В С++ уже давно есть constexpr. И это позволяет не только считать в компил-тайме адрес, но и проверять корректность введённых параметров. GCC на код на constexpr функции вычисления адреса по той же формуле, что и в макросе выдаёт тот же машинный код, но приведённые мною выражения вызывут ошибку на стадии компиляции.
+1
А что, макрос не может выдать предупреждение при недопустимых значениях?
Например, при превышении позиции бита например 31?
#define ALIAS_PTR(VarPtr,BitNum) \
*((__IO uint32_t *) (((uint32_t) &(VarPtr) & 0xf0000000UL) | 0x02000000UL \
+ (((uint32_t) &(VarPtr) & 0xfffffUL) << 5) | (((BitNum)>31)?(((BitNum)/0)):((BitNum)<< 2))))

Я конечно не хвалю макросы, но это часть нашей жизни)
0
Не в каждом случае. И всё равно не повод не юзать constexpr.
0
Не было случая чтобы вычисление с известными границами не позволило бы мне через макрос выдать предупреждение. Будь то вычисление позиции, вычисление скорости USART, значения пределов счетчиков… Если есть что считать и есть некие границы — почему бы мне не вставить в макрос проверку? Да, пусть в недопустимом случае даст предупреждение о делении на 0. Дешево и сердитто))
0
Однако, static_assert(BitNum <32); удобнее.
А ещё можно сделать проверку на содержание в маске только одного выставленного бита. Подчас приходится переводить 0x0200 в 9 для того же bit banding'а с использованием определений из CMSIS, и городить там такие проверки — это ад. Хотя да, язык макросов тьюринг-полный, и можно писать всё, что угодно, вопрос как.
-1
Ну, проверку наличия только одного бита и в макросе сделать лехко. Просто здесь это не требуется.
Здесь проблема ширше: в программе не должно быть разбросано вот таких обращений типа (GPIOB->CRL, 5). Все это делает простейшее изменение номера бита с 5 на 7 свирепым квестом по исходникам)
Все в исходнике должно быть только в символическом виде и изменение позиции бита должно меняться в одном месте.
Вот о чем надо говорить. А применять или не применять макросы — каждый решает сам.
+1
Хотя да, язык макросов тьюринг-полный
Разве? Я знаю что шаблоны С++ являются тьюринг-полными, а макросы С от этого очень далеки.
0
не очень. Хотя надо глянуть, чтото и меня сомненья гложат.
0
и как себя поведет constexp на этапе компиляции, если мы в цикле с неопределенными (на этапе компиляции) границами начнем биты выставлять? *биты выставлять — в контексте данной задачи.
0
Если компилятор не сможет вычислить значение на этапе компиляции – значение будет вычислено в runtime (если мы говорим о constexpr-функциях)
0
вот я так же подумал. это не замена для макросов. по крайней мере в данном случае.
0
Нет, это не замена макросам (ближе к замене макросов — это инлайн функции), это другой очень мощный механизм, который говорит компилятору, что значение функции может (и должно при определенных условиях) быть вычислено на этапе компиляции.
0
То есть, вы зачастую можете использовать constexpr-функциии вместо макросов (как в данном примере), но не это основная идея constexpr
0
GPIO->ODR = MY_MACROS(4,6); //считается в компилтайме
GPIO->ODR = myInlineFunction(4,6); //Сложение всё равно будет в рантайме, если вычисление сложное
GPIO->ODR = myConstexprFunction(4,6); //считается в компилтайме

Так что, всё таки, замена макросов.
0
Это замена макросов. Макрос тоже когда можно-считается в компилтайме, а когда нельзя — в рантайме. Инлайн функция будет всегда в рантайме.
-1
проблема не в том в рантайме он выполняется или при компиляции (прекомпиляции вернее). проблема в том что макрос это всегда inline, а constexpr? почитал, «constexpr functions and constexpr constructors are implicitly inline (7.1.2).» так что да, всегда inline.
0
А в чём проблема-то? constexpr — это современная замена устаревших и небезопасных макросов почти везде.
0
Инлайн функция будет всегда в рантайме.

Нет, вы ошибаетесь.
Вот пример

inline foo(int value) {
	return value + 10;
}

int bar(void) {
        return foo(1);   
}

Результат
00000008 <bar>:
   8:   e3a0000b        mov     r0, #11 ; 0xb
   c:   e12fff1e        bx      lr

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

Это работает по другому. Нельзя говорить что «макрос считается в компилтайме», там происходит следующее – происходит подстановка макроса (как и подстановка инлайн функции) дальше для полученного выражения (в примере выше это 1 + 10) компилятор строит expression tree. Он умеет упрощать expression tree, он видит, что в двух узлах дерева константы и упрощает дерево заменив их суммой. То бишь «считается в компилтайме» на стадии работы с деревом выражений, к этому моменту и макрос и иналайн функция будут внедрены в код, мы получим одинаковое деревья выражений и одинаковый результат (хотя там есть некие моменты, но можно их упустить)

Это замена макросов
Я говорю что constexpr и макрос – это 2 разные вещи, для разных целей, нельзя их сравнивать опираясь на частные случаи. Например, с помочью макросов можно конкатенировать токены (##) и т. д. в то время как у constexpr своя задача…
+1
Нет, вы ошибаетесь.
Вот пример
Это поведение — результат работы оптимизации, а не гарантированное стандартом.
А теперь попробуйте заменить вражение на что-либо сложное.
Нельзя говорить что «макрос считается в компилтайме»
Однако, в стандарте напрямую сказано, что #define a 4 + 5 должен быть заменён на 9.
Я говорю что constexpr и макрос – это 2 разные вещи, для разных целей, нельзя их сравнивать опираясь на частные случаи. Например, с помочью макросов можно конкатенировать токены (##) и т. д. в то время как у constexpr своя задача…
Вещи-то разные, а задача одна. Всё, что делают небезопасно на макросах можно и нужно делать безопасно при помощи таких keyword'ов, как const, constexpr, и, если совсем плохо, template.
Тут как с goto или с кастами. Каждый раз, когда хороший программист сталкивается с [goto,(char*),#define, ...] он должен переходить из режима кодинга в режим программирования. А это очень дорогой режим, требующий внимания и навыков. Если у вас пол кода на #define, то вырабатывается толерантность.
0
Однако, в стандарте напрямую сказано, что #define a 4 + 5 должен быть заменён на 9.

Может я ошибаюсь, но я не поманю такого (в каком именно разделе стандарта это указанно?)

В качестве теста прогоним код
#define a 4 + 5
int foo(void) {
	return a;
}

через препроцесор
arm-elf-gcc.exe -E test.c

получаем после препроцессора
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command line>"
# 1 "test.c"

int foo(void) {
 return 4 + 5;
}
Замены на 9 препроцессор не сделал, дальше компиляция и дерево выражений …
0
Хм… ок, буду почитать стандарт и оптимизацию.
0
Однако, в стандарте напрямую сказано, что #define a 4 + 5 должен быть заменён на 9.
Разве? Где? Насколько я знаю, препроцесор — чисто текстовый инструмент, подставляющий куски текста, и работающий независимо от компилятора. Всю подстановку констант выполняет компилятор.
Инлайны в этом плане отличаются только тем, что их подставляет сам компилятор и тем, что он может счесть подстановку не нужной.
+1
Для сравнения попробуйте в инлайне сделать тот же bit banding.
0
Вся разница в том, что Вы пишите про профессиональное занятие, и работу с большими объемами текста. Да, там будет другой подход. А в любительской практике — подойдет и макрос…
Тем более задачу — решает, код получается — компактный. Если мне понадобится контроль входных значений — возьму уже готовую функцию API из всеми нелюбимого HAL.
HAL функцией не воспользовался из-за её тяжеловесности, а мне нужна была работа самопальной шины данных (посаженные на 1 порт выхода SPI от 14 ADC, входы — вместе ...) на 2-3 Мгц, через этот макрос реализован самопальный многовходный SPI.
0
Набросал тестовый пример (по мотивам того, что в посте)

#define SRAM_BB_BASE          ((unsigned)0x22000000) /*!< SRAM base address in the bit-band region */
#define SRAM_BASE             ((unsigned)0x20000000) /*!< SRAM base address in the alias region */

#define MAILBOX   0x20004000

#if 0
#define BITBAND_SRAM(a,b) ((SRAM_BB_BASE + (a-SRAM_BASE)*32 + (b*4))) 

#else
inline unsigned BITBAND_SRAM(unsigned a, unsigned b) {
	return ((SRAM_BB_BASE + (a-SRAM_BASE)*32 + (b*4)));
}
#endif

unsigned foo(void) {
	return  *((unsigned *) BITBAND_SRAM(MAILBOX,0));
}

arm-elf-gcc.exe -mcpu=arm7tdmi -O -Wall -c test.c

В обоих случаях (что с макросом, что с инлайн) результат одинаков (притом одинаково недооптимизированный :)
00000010 <foo>:
  10:   e3a03422        mov     r3, #570425344  ; 0x22000000
  14:   e2833702        add     r3, r3, #524288 ; 0x80000
  18:   e5930000        ldr     r0, [r3]
  1c:   e12fff1e        bx      lr
0
А в чем недооптимизированность?
0
Он не полностью вычислил адрес на этапе компиляции остался один add в рантайме, хотя мог бы и от него избавиться, т. к. оба операнда константы.
0
Судя по бинарному коду, не мог. Первая команда — не MOV r3, #570425344, а MOV r3, 0x22 LSH 24.
+1
Да, согласен, что-то я протупил…
0
Я конечно разделяю вашу идею с использованием constexpr, это действительно мощное средство. Но боюсь для многих вариант «бросайте все и переходите на С++11» это слишком радикально :) Еcли речь идет только о контроле типов на этапе компиляции – достаточно заменить макрос на инлайн функцию. Плюс не нужно будет париться с другими проблемами макросов (типа оборачивания в do {} while(0))
+1
Для тех, кто пользуется GCC переход вышлядит просто:
1) добавляем две буковки p к исходнику
2) обрамляем все инклюды C-файлов в исходнике в блок extern «C» {...}
3) добавляем флаг -std=c++11 или -std=c++1y
4) иногда (не в code::blocks, не в eclipse) надо ещё поменять gcc на g++, но чаще всего это происходит само.

Пока тебе не нужен плюсовый рантайм, а он нужен для классов, для виртуального наследования и прочего трешака всё крайне просто.
0
тоже самое можно сказать про всю концепцию CMSIS, они сами пишут, что да, стандарту не соответствует именно из за применения макросов. Однако все пользуются и считают это удобным.
GPIOB->BRR = «jhjh»; — можно попробовать и в стандартном CMSIS написать.
Давайте дружно обругаем CMSIS и пересядем на HAL, который на писан с использованием того же CMSIS, зато большую часть кода занимают проверки допустимости выделенных значений. ИМХО.
0
GPIOB->BRR = «jhjh»
Нельзя. Смотри объявление.
typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

У CMSIS очень мало чего реализовано макросами. И не пользоваться этим вполне возможно. Я не пользуюсь.
0
а вообще тема довольно избитая, и прежде чем писать статью, стоило бы воспользоваться поиском. Не так давно тоже проскальзывала тема, еще одна описана мной. Дежа-вю.
0
Не знаю, насколько избитая тема — именно такого в поиске не нашел. Выше уже писал, что всё что можно уже придумано до нас, и это — велосипед. Для кого-то решение очевидно, а кто-то подсмотрит. В поиске — ответа не нашел, искать — умею…
А вот что бы кто-либо из начинающих, как и я(знаете, как обидно это говорить в 43 — начинающий?), не искал долго — написал, думаю лишним не будет. Тем более подобная работа с указателями в учебниках, как правило, не рассматривается.
0
В поиске — ответа не нашел, искать — умею…
Статья
знаете, как обидно это говорить в 43 — начинающий?
Главное, что вы решились им быть.))
0
Статья
, значит не совсем умею… но я же не списал, а написал своё, на основе примера от разработчика ядра — ARM…
Главное, что вы решились им быть.))

Особенно обидно, что это после получения в 94 году профильного образования (нынче — НГТУ). Правда ни дня по специальности не работал. И компьютер потом увидел только в 2001 году, их просто не было на моём горизонте… а о МК вообще вспомнил только в 2011 году.
0
А что плохого быть начинающим то? У меня вон отец в прошлом году на пенсию вышел, открыл для себя комп, а точнее ютуб. До этого и пальцем клавиатуры в жизни не касался. Теперь переваривает и пробует в деле открывшиеся ему огромные объемы инфы по его хобби — рыбалке, лесу и прочему. Говорит, что в небо глянуть некогда, а вроде пенсионер =)
+3
Правильно сделали что взялись. Через 5 лет будете гуру)
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.