Введение
Внимание, это не очередная «Hello world»статья о том как помигать светодиодом или попасть в свое первое прерывание на STM32. Однако, я постарался дать исчерпывающие объяснения по всем затрагиваемым вопросам, поэтому статья будет полезна не только многим профессиональным и мечтающим стать таковыми разработчикам (как я надеюсь), но и начинающим программистам микроконтроллеров, так как тема эта почему-то обходится стороной на бесчисленных сайтах/блогах «учителей программирования МК».
Почему я решил это написать?
Хоть я и преувеличил, сказав ранее, что аппаратный bit banding семейства Cortex-M не описывается на специализированных ресурсах, все же есть места, где это возможность освещается (и даже тут встречал одну статью), но эта тема явно нуждается в дополнении и осовременивании. Отмечу, что это касается и англоязычных ресурсов. В следующем разделе я объясню, почему эта возможность ядра может быть чрезвычайно важной.
Теория
(а те кто с ней знаком, могут прыгать сразу в практику)
Аппаратный bit banding — это особенность самого ядра, поэтому и не зависит от семейства и фирмы производителя микроконтроллеров, главное чтобы ядро было подходящее. В нашем случае пусть это будет Cortex-M3. Следовательно, и информацию по этому вопросу следует искать в официальном документе на само ядро, и такой документ есть, вот он, раздел 4.2 подробно описывает как пользоваться этим инструментом.
Тут я хотел бы сделать небольшое техническое отступление для программистов, не знакомых с ассемблером, коих сейчас большинство, ввиду пропагандируемой сложности и бесполезности ассемблера для таких «серьёзных» 32битных микроконтроллеров, как STM32, LPC и т. д. Более того, нередко можно встретить попытки порицания за использования ассемблера в этой области даже на хабре. В этом разделе я хочу кратко описать механизм записи в память МК, которой и должен прояснить преимущества bit banding.
Поясню на конкретном простом примере для большинства STM32. Допустим, мне нужно превратить PB0 в выход общего назначения. Обычное решение будет выглядеть так:
GPIOB->MODER |= GPIO_MODER_MODER0_0;
Очевидно, мы используем побитовое «ИЛИ», чтобы не затереть остальные биты регистра.
Для компилятора это транслируется в следующий набор из 4х инструкций:
- Загрузить в регистр общего назначения(РОН) адрес GPIOB->MODER
- Загрузить в другой РОН значения по адресу, указанному в РОН из п1.
- Сделать побитовое «ИЛИ» этого значения с GPIO_MODER_MODER0_0.
- Загрузить результат обратно в GPIOB->MODER.
Также нельзя забывать, что данное ядро использует набор инструкция thumb2, а значит они могут быть разными по объёму. Отмечу также, что везде речь идёт об уровне оптимизации O3.
На языке ассемблера это выглядит вот так:
Видно, что самая первая инструкция есть ни что иное, как псевдо-инструкция со смещением, находим по адресу PC(учитывая конвеерность) + 0x58 значение адреса регистра.
Получается у нас 4 шага (а тактов еще больше) и 14 байт занимаемой памяти на одну операцию.
Если вы хотите больше знать об этом, то рекомендую книгу [2], кстати, есть и на русском.
Переходим к методу bit_banding.
Суть, по крестьянски, в том, что в процессоре есть специально выделенная область памяти, записывая значения в которую, мы не меняем другие биты регистра периферии или RAM. То есть нам не нужно выполнять пункты 2) и 3), описанные выше, и для этого достаточно лишь пересчитать адрес по формулам из [1].
Пробуем проделать аналогичную операцию, ее ассемблер:
Пересчитанный адрес:
Тут у нас добавилась инструкция записи #1 в РОН, но все равно, в итоге получается 10 байт, вместо 14, и на пару тактов меньше.
А что и с того, раз разница то смешная?
С одной стороны, экономия не существенна, особенно в тактах, когда уже заведено за привычку разгонять контроллер до 168МГц. В среднем проекте, моментов, где можно применить этот метод будет 40 — 80, соответственно в байтах экономия может достигать 250 байт, если адреса будут различаться. А если учесть, что «зашкваром» сейчас считается программирование МК напрямую на регистрах, а «круто» использовать всякие кубики
Также, цифра 250 байт искажена тем, что в сообществе активно используются высоко-уровневые библиотеки, прошивки раздуваются до неприличных размеров. Программируя же на низком уровне, это как минимум 2 — 5% объема ПО для среднестатистического проекта, с грамотной архитектурой и O3 оптимизацией.
Опять же, я не хочу сказать, что это какой-то супер-пупер-мега крутой инструмент, которым должен пользоваться каждый уважающий себя программист МК. Но если я могу сократить расходы даже на столь малую часть, то почему бы этого не сделать?
Реализация
Все варианты будут даны только для настройки периферии, так как мне не попадалась ситуация, когда это было бы нужно для RAM. Строго говоря, для RAM формула похожа, достаточно лишь поменять базовые адреса для расчёта. Так как же это реализовать?
Ассемблер
Пойдём с самых низов, с моего любимого Ассемблера.
На ассемблерных проектах, я обычно выделяю пару 2байтных(по работающих с ними инструкциями) РОН под #0 и #1 на весь проект, и использую их также в макросах, что сокращает мне еще 2 байта на постоянной основе. Ремарка, на Ассемблер для STM я CMSIS не нашёл, потому в макрос кладу сразу номер бита, а не его значение по регистру.
Реализация для GNU Ассемблера
@Захардкориваю два РОНа.
MOVW R0, 0x0000
MOVW R1, 0x0001
@Макрос установки бита
.macro PeriphBitSet PerReg, BitNum
LDR R3, =(BIT_BAND_ALIAS+(((\PerReg) - BIT_BAND_REGION) * 32) + ((\BitNum) * 4))
STR R1, [R3]
.endm
@Макрос сброса бита
.macro PeriphBitReset PerReg, BitNum
LDR R3, =(BIT_BAND_ALIAS+((\PerReg - BIT_BAND_REGION) * 32) + (\BitNum * 4))
STR R0, [R3]
.endm
Примеры:
Примеры для Ассемблера
PeriphSet TIM2_CCR2, 0
PeriphBitReset USART1_SR, 5
Безусловный плюс этого варианта в том, что мы имеем полный контроль, чего нельзя сказать о дальнейших вариантах. И как покажет последний раздел статьи, плюс этот очень весомый.
Однако, никому не нужны проекты для МК на Ассемблере, примерно с конца нулевых, а значит нужно переходить на СИ.
Plain C
Честно говоря, простой Сишный вариант был найден мной в начале пути, где-то на просторах сети. На тот момент я уже реализовал bit banding на Ассемблере, и случайно наткнулся на C файл, он сразу заработал и я решил ничего не изобретать.
Реализация для plain C
/*!<=================PLAIN C SECTION========================>!*/
#define MASK_TO_BIT31(A) (A==0x80000000)? 31 : 0
#define MASK_TO_BIT30(A) (A==0x40000000)? 30 : MASK_TO_BIT31(A)
#define MASK_TO_BIT29(A) (A==0x20000000)? 29 : MASK_TO_BIT30(A)
#define MASK_TO_BIT28(A) (A==0x10000000)? 28 : MASK_TO_BIT29(A)
#define MASK_TO_BIT27(A) (A==0x08000000)? 27 : MASK_TO_BIT28(A)
#define MASK_TO_BIT26(A) (A==0x04000000)? 26 : MASK_TO_BIT27(A)
#define MASK_TO_BIT25(A) (A==0x02000000)? 25 : MASK_TO_BIT26(A)
#define MASK_TO_BIT24(A) (A==0x01000000)? 24 : MASK_TO_BIT25(A)
#define MASK_TO_BIT23(A) (A==0x00800000)? 23 : MASK_TO_BIT24(A)
#define MASK_TO_BIT22(A) (A==0x00400000)? 22 : MASK_TO_BIT23(A)
#define MASK_TO_BIT21(A) (A==0x00200000)? 21 : MASK_TO_BIT22(A)
#define MASK_TO_BIT20(A) (A==0x00100000)? 20 : MASK_TO_BIT21(A)
#define MASK_TO_BIT19(A) (A==0x00080000)? 19 : MASK_TO_BIT20(A)
#define MASK_TO_BIT18(A) (A==0x00040000)? 18 : MASK_TO_BIT19(A)
#define MASK_TO_BIT17(A) (A==0x00020000)? 17 : MASK_TO_BIT18(A)
#define MASK_TO_BIT16(A) (A==0x00010000)? 16 : MASK_TO_BIT17(A)
#define MASK_TO_BIT15(A) (A==0x00008000)? 15 : MASK_TO_BIT16(A)
#define MASK_TO_BIT14(A) (A==0x00004000)? 14 : MASK_TO_BIT15(A)
#define MASK_TO_BIT13(A) (A==0x00002000)? 13 : MASK_TO_BIT14(A)
#define MASK_TO_BIT12(A) (A==0x00001000)? 12 : MASK_TO_BIT13(A)
#define MASK_TO_BIT11(A) (A==0x00000800)? 11 : MASK_TO_BIT12(A)
#define MASK_TO_BIT10(A) (A==0x00000400)? 10 : MASK_TO_BIT11(A)
#define MASK_TO_BIT09(A) (A==0x00000200)? 9 : MASK_TO_BIT10(A)
#define MASK_TO_BIT08(A) (A==0x00000100)? 8 : MASK_TO_BIT09(A)
#define MASK_TO_BIT07(A) (A==0x00000080)? 7 : MASK_TO_BIT08(A)
#define MASK_TO_BIT06(A) (A==0x00000040)? 6 : MASK_TO_BIT07(A)
#define MASK_TO_BIT05(A) (A==0x00000020)? 5 : MASK_TO_BIT06(A)
#define MASK_TO_BIT04(A) (A==0x00000010)? 4 : MASK_TO_BIT05(A)
#define MASK_TO_BIT03(A) (A==0x00000008)? 3 : MASK_TO_BIT04(A)
#define MASK_TO_BIT02(A) (A==0x00000004)? 2 : MASK_TO_BIT03(A)
#define MASK_TO_BIT01(A) (A==0x00000002)? 1 : MASK_TO_BIT02(A)
#define MASK_TO_BIT(A) (A==0x00000001)? 0 : MASK_TO_BIT01(A)
#define BIT_BAND_PER(reg, reg_val) (*(volatile uint32_t*)(PERIPH_BB_BASE+32*((uint32_t)(&(reg))-PERIPH_BASE)+4*((uint32_t)(MASK_TO_BIT(reg_val)))))
Как видно, очень простой и прямолинейный кусок кода, написанный на языке пре процессора. Основная работа здесь — это перевод CMSIS значений в номер бита, которая отсутствовала как необходимость в ассемблерном варианте.
Ах да, пользоваться этим вариантом так:
Примеры для plain C
BIT_BAND_PER(GPIOB->MODER, GPIO_MODER_MODER0_0) = 0; //Сброс
BIT_BAND_PER(GPIOB->MODER, GPIO_MODER_MODER0_0) = 1; //Установка (!0)
Однако современные (массово, по моим наблюдениям, примерно с 2015г) тенденции идут в пользу замены С на С++ даже для МК. Да и макросы не самый надёжный инструмент, поэтому предначертано было родиться следующей версии.
Cpp03
Тут всплывает очень интересный и обсуждаемый, но мало используемый в виду своей сложности, с одним заезженным примером факториала, инструмент — метапрограммирование.
Ведь задача перевода значения переменной в номер бита идеальна(значения то в CMSIS уже есть), и в данном случае практична, для compile time.
Я реализовал это следующим образом, при помощи шаблонов:
Реализация для С++03
template<uint32_t val, uint32_t comp_val, uint32_t cur_bit_num> struct bit_num_from_value
{
enum { bit_num = (val == comp_val) ? cur_bit_num : bit_num_from_value<val, 2 * comp_val, cur_bit_num + 1>::bit_num };
};
template<uint32_t val> struct bit_num_from_value<val, static_cast<uint32_t>(0x80000000), static_cast<uint32_t>(31)>
{
enum { bit_num = 31 };
};
#define BIT_BAND_PER(reg, reg_val) *(reinterpret_cast<volatile uint32_t *>(PERIPH_BB_BASE + 32 * (reinterpret_cast<uint32_t>(&(reg)) - PERIPH_BASE) + 4 * (bit_num_from_value<static_cast<uint32_t>(reg_val), static_cast<uint32_t>(0x01), static_cast<uint32_t>(0)>::bit_num)))
Пользоваться можно аналогично:
Примеры для C++03
BIT_BAND_PER(GPIOB->MODER, GPIO_MODER_MODER0_0) = false; //Сброс
BIT_BAND_PER(GPIOB->MODER, GPIO_MODER_MODER0_0) = true; //Установка
А почему же остался макрос? Дело в том, что я не знаю другого способа гарантированно вставить данную операцию без перехода в другую область кода программы. Буду очень рад, если в комментариях подскажут. Ни шаблоны, ни inline функции такой гарантии не дают. Да и макрос тут справляется со своей задачей на отлично, нет смысла его менять только потому, что
Удивительно, но время все еще не стояло на месте, компиляторы все активнее поддерживали C++14/C++17, почему бы не воспользоваться новшествами, сделав код более понятным.
Cpp14/Cpp17
Реализация для C++14
constexpr uint32_t bit_num_from_value_cpp14(uint32_t val, uint32_t comp_val, uint32_t bit_num)
{
return bit_num = (val == comp_val) ? bit_num : bit_num_from_value_cpp14(val, 2 * comp_val, bit_num + 1);
}
#define BIT_BAND_PER(reg, reg_val) *(reinterpret_cast<volatile uint32_t *>(PERIPH_BB_BASE + 32 * (reinterpret_cast<uint32_t>(&(reg)) - PERIPH_BASE) + 4 * (bit_num_from_value_cpp14(static_cast<uint32_t>(reg_val), static_cast<uint32_t>(0x01), static_cast<uint32_t>(0)))))
Как видно, я просто заменил шаблоны на рекурсивную constexpr функцию, что на мой взгляд, человеческому глазу это более понятно.
Пользоваться аналогично. Кстати, в С++17 по идее можно использовать рекурсивную лямбда constexpr функцию, но не уверен, что это приведет хоть к каким-то упрощениям, а также не усложнит ассемблерный порядок.
Резюмируя, все три С/Cpp реализации дают одинаково корректный набор инструкций, согласно разделу «Теория». Работаю давно со всеми реализациями на IAR ARM 8.30 и gcc 7.2.0.
Практика is a bitch
Вот все, кажется, и сложилось. Экономию памяти подсчитали, реализацию выбрали, готовы улучшать производительность. Не тут то было, тут как раз случай расхождения теории и практики. А когда было по-другому?
Я бы никогда не опубликовал это, если бы не протестировал, а насколько реально на проектах уменьшается занимаемый объем. Я специально на паре старых проектов заменил этот макрос на обычную реализацию без маски, и посмотрел разницу. Результат удивил неприятно.
Как оказалось, объём практически не меняется. Я специально выбрал проекты, где как раз и использовалось по 40-50 подобных инструкций. Согласно теории, я должен был сэкономить ну как минимум 100 байт, а как максимум 200. На практике же, разница получилось 24 — 32 байта. Но почему?
Обычно, когда настраиваешь периферию, настраиваешь 5-10 регистров практически подряд. И на высоком уровне оптимизации, компилятор не располагает инструкции точно в порядке следования регистров, а располагает инструкции так, как ему кажется правильным, порой мешая их, в казалось бы, неразрывных местах.
Я вижу два варианта(тут мои домыслы):
- Либо компилятор настолько умен, что за тебя знает как будет лучше оптимизировать сет инструкций
- Либо компилятор все же пока еще не умнее человека, и сам себя путает, встречая такие конструкции
То есть получается, этот метод на языках «высокого уровня» при высоком уровне оптимизации отрабатывает корректно только в том случае, если по-близости с одной такой операцией нету похожих операций.
Кстати говоря, на уровне O0 теория с практикой сходятся в любом случае, но мне этот уровень оптимизации не интересен.
Резюмирую
Отрицательный результат — тоже результат. Я думаю, каждый сделает выводы для себя сам. Лично я так и продолжу использовать данный прием, хуже от него точно не будет.
Надеюсь было интересно и хочу выразить огромнейший респект тем, кто дочитал до конца.
Перечень литературы
- «Cortex-M3 Technical Reference Manual», раздел 4.2, ARM 2005.
- The definitive guide to the ARM Cortex-M3, Joseph Yiu.
P.S У меня в запасе мешок мало освещенных тем, связанных с разработкой встраиваемой электроники. Дайте знать, если интересно, буду потихоньку доставать их.
P.P.S. Как-то секции кода кривовато получилось вставить, подскажите пожалуйста, как улучшить, если это возможно. В целом, можно скопировать интересующий кусок кода в notepad и избежать неприятных эмоций при анализе.