Как стать автором
Обновить

Аппаратный bit banding CortexM3/M4(ARM), архитектура ядра, ассемблер, С/C++14 и капля метапрограммирования

Время на прочтение 9 мин
Количество просмотров 23K
Всего голосов 77: ↑73 и ↓4 +69
Комментарии 38

Комментарии 38

Спасибо, очень интересно! То есть разработчики процессора заложили возможность управления отдельными битами, а разработчики версии языка сделали, как привыкли на больших машинах. В некоторых версиях С (например. от MikroE) есть управление отдельными битами по виду GPIOB_ODR.B13=1 (т.е. обращение к 13-му биту GPIOB_ODR), но все же это экзотика.
Отлично, кстати, IAR 8.40.1 вышел с С++17). Да еще к этому решению это не имеет отношения, но количество constexpr рекурсий в ИАР ограничено, я наткнулся на это, когда сделал tuple с количеством элементов больше 64. Мне выдалось, что превышен порог количества рекурсий. Это конечно можно в настройках поменять, но неприятно, когда думаешь, что все работает, а потом бах в один прекрасный момент и код не компилится.
Спасибо, строго говоря, с рекурсиями шаблонов столкнулся c ограничением вложенности, именно поэтому в коде для 03 стандарта есть отдельный шаблон для 31ого бита, иначе рекурсия уходит в бесконечность(сталкивался для gcc).
А по теме в общем, я могу ошибаться, но вроде как стандарт языка позволяет компилятору конвертировать constexpr функцию в run time, если ему кажется это правильным. Поэтому я всегда очень осторожно к ним отношусь и проверяю каждый случай применения.
НЛО прилетело и опубликовало эту надпись здесь

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

Вот только он точно также преотлично ругнётся и на значения, заданные в компайл-тайме.

static_assert вы можете сделать только «снаружи» — а там достаточно присвоить значение constexpr-переменной, чтобы гарантировать, что всё будет вычислено в компайл-тайме. Даже в -O0. Собственно вот. Сравните код bar и baz

godbolt — ваш друг а таких вещах.
Спасибо за пояснения! Я обычно и стараюсь такую функцию приравнять к constexpr переменной.
Автор забыл упомянуть про ключевую особенность этого bit banding, для которой он собственно и был реализован в железе (да, а совсем не для экономии памяти). Bit banding — атомарный. Между вычиткой ячейки памяти и последующей записью поксоренного содержимого исходное значение может измениться. В результате запись модифицированного содержимого затрёт произошедшие изменения. Для того, чтобы такого не происходило, и используется описываемая технология манипуляции отдельными битами.
Ну не то чтобы забыл, статья и должна была осветить только аспект затрачиваемой памяти и тактов.
Однако я не считаю верным утверждение, что основная особенность — атомарность операции. Я думаю экономия памяти по значимости не уступает ей.

На запрещение/разрешение прерываний уходит более чем достаточно как байтов кода, так и таков.
Думаю вам стоит поправить/доработать статью.

Что конкретно по-вашему мнению мне следует поправить? Это не наезд, как можно подумать, я правда не понял из вашего комментария.

По теме байтов, вы приводите ссылку на другую версию ядра. В cortex-m3 это всего-лишь одна инструкция CPSIE/ CPSID.
Но дело то даже и не в этом. На счёт той же атомарности, в STM32, к примеру, почти все биты, что могут поменяться из вне, вынесены в отдельный Read-Only регистр (ANY_PERIPH)->SR. Если подобные биты есть в других регистрах, то обычно сбросить подобные флаги можно только записью единицы, что не нарушит логику программы даже при неатомарной операции.

Видимо есть недопонимание "атомарности".


В случае управления "выходами" GPIO речь не о memory barries и/или memory ordering, а о потенциальном переключении процессора на другую задачу и/или обработчик прерываний, которые могут обращаться к тому-же GPIO. Соответственно будет прелестный heisenbug, если переключение произойдет между LDR и STR.


Кроме этого, CPSIE не всегда подходит, например если прерывания были запрещены выше по стеку вызовов для более широкого контекста. Соответственно, нередко перед CPSID приходится сохранять маску, а вместо CPSIE ёё восстанавливать.

Забавно, кстати, что это всё — следствие RISC'овости ARM'а. В славном 80186 ничего этого было не нужно: можно было просто одной обычной командой флипать биты без всех этих извращений… «прогресс» называется.
Вы ошибаетесь или выдаете свое желаемое за действительное. Я, очевидно, знаю далеко не все, но вот это
В случае управления «выходами» GPIO речь не о memory barries и/или memory ordering, а о потенциальном переключении процессора на другую задачу и/или обработчик прерываний, которые могут обращаться к тому-же GPIO. Соответственно будет прелестный heisenbug, если переключение произойдет между LDR и STR.
мне известно. И я хотел бы знать, почему вы посчитали, что этого понимания у меня нет?

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

А вот вас я действительно не очень понял. CPSIE глобальная команда, не зависит от места в стеке вызовов и разрешает прерывания для проца в целом.

Вы также сказали, что статью следует поправить или дополнить, и я по прежнему хочу знать, как именно, по вашему мнению, это нужно сделать, поскольку вы опять не сказали, что именно вас не устраивает.
Такое обычно называют не здоровой критикой.
И я хотел бы знать, почему вы посчитали, что этого понимания у меня нет?
У вас возможно, такое понимание есть. А вот у читателя — далеко не факт. Вы же сами пишите о том, что статей про это дело мало.

И с учётом этого делать акцент в статье на «побочную», в общем-то, фичу — экономию нескольких байт — не упомянув основную… ну как-то странно.

CPSIE глобальная команда, не зависит от места в стеке вызовов и разрешает прерывания для проца в целом.
Что не очень-то хорошо, если вызывающая вас процедура этого не ожидает. Представьте себе, что вам нужна процедура, которая должна «атомарно» флипнуть бит, но может быть вызвана как из обработчика прерываний (где разрешать прерывания, до того, как вы к этому готовы не слишком-то хорошо), так и из обычного кода. «Бездумно» включать прерывания в таком случае не получится, а разводить две копиии… ну можно, наверное, на шаблонах — но тут вы можете много-таки кода в итоге надублировать…
И с учётом этого делать акцент в статье на «побочную», в общем-то, фичу — экономию нескольких байт — не упомянув основную… ну как-то странно.

С этой точки зрения вы правы конечно, тут мне и возразить то нечего, а в статью я этот момент добавил уже ранее. Для себя учел на будущее, если взялся тему раскрывать, то описать следует все важные особенности.
  • Использование bit banding для GPIO даёт чуть больше удобства/выгоды, чем подсчитано в статье.
  • Потому-что операции bit banding атомарны, что избавляет от необходимости запрещать/разрешать прерывания для изменения отдельных бит.
  • При этом затраты на запрещение/разрешения прерываний, как правило, больше чем просто пара инструкций CPSID/CPSIE.
  • Потому-что непосредственно использование CPSIE в большом/сложном проекте (и/или в повторное используемом коде), как правило, недопустимо.
  • Потому-что прерывания могут быть уже запрещены ранее по стеку вызова или потоку выполнения кода и будут разрешены позже, т.е. прерывания могут быть запрещены для более широкого контекста, внутри которого может быть вызван ваш GPIO-код.

Соответственно, вместо__disable_irq()/__enable_irq() требуется сохранять и восстанавливать маску прерываний или флаги CPU. Например, см local_irq_save() / local_irq_restore(). Но при использовании bin banding всего этого можно избежать, как минимум использовать реже (при операциях над несколькими регистрами и т.п.).

Использование Bit banding для GPIO в частности и для периферии в целом, как правило, не дает ничего. Все давным давно решено на уровне организации периферии. Например, проблемы с GPIO решаются через SET/RST регистры (BSRR в случае stm32).

Ну, для исключения таких ситуаций есть эксклюзивные чтение и запись. Так что вряд ли bit banding только ради этого придуман.

Ещё раз: ARM — это RISC. Там нет атомарных инструкций. За исключением SWP (что, всё-таки, немного не то).

Bit banding как раз и позволяет реализовать захват шины и атомарную модификацию данных — никакого другого способа на RISC процессорах нету (придумать-то можно, но практически — нету).
Не, стоп. Я комментировал конкретное сообщение, в котором было следующее:
Между вычиткой ячейки памяти и последующей записью поксоренного содержимого исходное значение может измениться. В результате запись модифицированного содержимого затрёт произошедшие изменения. Для того, чтобы такого не происходило


Так вот, чтобы «такого не происходило» — есть LDREX/STREX. Вот собсна и все, атомарные инструкции тут приплетать незачем, тем более что bit banding выглядит менее универсальным средством, чем LL/SC.

И, к слову, чем вам не понравилась SWP, если уж мы тут все равно извращаемся? Ну, кроме того, что она deprecated. Формально отлично подходит для записи значения.
Так вот, чтобы «такого не происходило» — есть LDREX/STREX.
Про которые мануал говорит следующее: Load-Exclusive and Store-Exclusive must only access memory regions marked as Normal — то есть для GPIO они неприменимы.

Вот собсна и все, атомарные инструкции тут приплетать незачем, тем более что bit banding выглядит менее универсальным средством, чем LL/SC.
Зато он работает — в отличие от LL/SC. Которые, как бы, для совсем-совсем другого.

И, к слову, чем вам не понравилась SWP, если уж мы тут все равно извращаемся?
Тем, что она не работает?
Формально отлично подходит для записи значения.
«Записать не глядя» — да. Но вот беда: единственный способ её использования — это изменить значение, понять, что мы «натворили делов» и быстро-быстро всё исправить. Если вы реализуете какую-нибудь многопоточность — этого, в общем и целом, достаточно. Если вы «дёргаете» оборудование, то случайные скачки туда-сюда на линиях могут к разным очень странным эффектам на подключённом оборудовании производить…
то есть для GPIO они неприменимы


Весомо, упустил.

Зато он работает — в отличие от LL/SC. Которые, как бы, для совсем-совсем другого.


Буквально для того же, но, как стало известно, не подходят для портов ввода-вывода.

«Записать не глядя» — да


Запись в порт обычно именно так и происходит. Заметьте, что я только про запись и говорил.
Запись в порт обычно именно так и происходит.
Нет. Если вы используете bit banding — то вы читаете/пишите (поднимаете бит), читаете/пишите (поднимаете второй).

А с SWP вы вначане читаете, потом прерывание, внова прочитали и подняли бит, потом опустили первый, подняли второй, потом поняли, что натворили — и подняли оба. Вот этого «дёрг-дёрг» хотелось бы избежать — и именно это является преимуществом bit banding'а.
Если нужно устанавливать/сбрасывать отдельные биты — то bit banding является лучшим выбором и по сути для этого сделан. Если же говорить о «записи вообще», без привязки именно к дерганию битов, о чем собственно я и писал постом выше, то и SWP достаточно.

Предлагаю дискуссию завершить, а то по третьему кругу пошли. Думаю все всё уже поняли.
Следует отметить что bit banding уже отсутствует в М7, мне пришлось переписывать часть кода по этой бяке. После чего я выяснил что структуры, перечисления и прочие гадости — дают намного больший эффект, чем дёрганье отдельных битов.
Небольшой примерчик для глобальных флагов — forum.easyelectronics.ru/viewtopic.php?f=35&t=22572
Слышал об этом, но как-то не довелось с семёрками поработать. Спасибо за ссылку, оставлю себе на заметку для подходящего случая.
M0 от STM так-же не поддерживает bit banding ( видимо ввиду непопулярности ).
А так удобно было.
Приходиться отказываться от него.
в С++17 по идее можно использовать рекурсивную лямбда constexpr функцию, но не уверен, что это приведет хоть к каким-то упрощениям, а также не усложнит ассемблерный порядок.

Зачем лямбды, у вас и так уже как-то всё очень сложно — рекурсия, дополнительные аргументы…
Даже на C++14 уже вполне можно так:

constexpr uint32_t bit_num_from_value_cpp14_v2(uint32_t val)
{
    uint32_t i = 0;
    while (1 << i != val) ++i;
    return i;
}
Можно и так конечно, может это влияние шаблона, но мне почему-то эти способы равнозначны по восприятию.
О, всегда приятно встретить эксперта.
Вы хотите сказать, что для комплятора в compile-time цикл феерически сложнее, чем рекурсия, поэтому время компиляции будет в разы больше, я правильно понимаю?
Несколько лет узнал об этой, казалось бы, классной фиче из книги «The Designer’s Guide to the
Cortex-M Processor Family». Думая о том, какие возможности это дает, добавил макросы, похожие на представленные в статье, в свои библиотеки. В результате, за два года ими никто не пользовался, включая меня(((
У этого конечно есть применение по мимо настройки периферии, например можно дать какому-то классу «указатель на бит», управляющий уровнем GPIO. Но как оказалось, в работе этого не достаточно.
Вообще у ядра есть более крутые возможности вроде:
  • Перенос таблицы прерываний в ОЗУ (начиная с cortex-M0+). Очень удобно, когда куски кода сами назначают себе прерывание.
  • Программные прерывания и вызов прерываний командой — это позволяет, например, писать тесты.
  • Математика с насыщением(Saturated Maths Instructions), которая очень хороша при работе с ПИД-регуляторами или цифровыми фильтрами.

Разумеется есть куча тругих, не менее полезных возможностей, вроде проверки указателя на валидность.
НЛО прилетело и опубликовало эту надпись здесь
Буду знать, спасибо!
Эта фишка нужна для реализации классического ПЛК с языками IEC 61131-3, вроде Ladder. Там основная память — битовая и почти все операции — битовые. Так что такая модель памяти удешевляет трансляцию и прилично увеличивает быстродействие.

Видимо планировалось массовое производство ПЛК на основе Cortex-M.
В свежих инклудах для stm32 нынче есть номера всех битов. Потому достаточно вот такого:
__attribute__ ((always_inline)) static inline uint32_t BBIO_RD(volatile uint32_t * addr, uint8_t bitnum) 
{
	volatile uint32_t * bitptr;
	bitptr = ((uint32_t *)( (((uint32_t)addr)-(0x40000000UL))*32 + bitnum*4 + (0x42000000UL) ));
	return *bitptr;
}

__attribute__ ((always_inline)) static inline void BBIO_WR(volatile uint32_t * addr, uint8_t bitnum, uint32_t value)
{
	volatile uint32_t * bitptr;
	bitptr = ((uint32_t *)( (((uint32_t)addr)-(0x40000000UL))*32 + bitnum*4 + (0x42000000UL) ));
	*bitptr = value;
}

и использование типа такого:
BBIO_WR(&ADC1->CR2,ADC_CR2_ADON_Pos,1);


Благодаря __attribute__ ((always_inline)) функции всегда инлайнятся, а благодаря static — не генерятся отдельно.

Что касается того, почему не сильно сокращается размер, тут всё просто.
Обычно в один блок периферии делается много записей подряд, потому компилятор сначала одной командой грузит адрес начала ячеек блока в регистр, а потом использует его несколько раз в адресации со смещением, например:

        // setup ADC
        ADC1->CR1 = ADC_CR1_SCAN | (0<<ADC_CR1_DUALMOD_Pos);
  42:   4b13            ldr     r3, [pc, #76]   ; (90 <adc_init+0x90>)
  44:   f44f 7280       mov.w   r2, #256        ; 0x100
  48:   605a            str     r2, [r3, #4]
        ADC1->CR2 |= ADC_CR2_CONT | ADC_CR2_DMA | (0<<ADC_CR2_ALIGN_Pos);
  4a:   689a            ldr     r2, [r3, #8]
  4c:   f442 7281       orr.w   r2, r2, #258    ; 0x102
  50:   609a            str     r2, [r3, #8]
«Сложна»
Для CUBEIDE использую такое
//STM32F411
#define GPIO_OUT_REGISTER_OFFSET 0x14UL
#define GPIO_IN_REGISTER_OFFSET 0x10UL

#define PRT_WR(x, y) *((uint32_t *)((GPIO##x##_BASE - PERIPH_BASE + GPIO_OUT_REGISTER_OFFSET)*32 + PERIPH_BB_BASE + y * 4))
#define PRT_RD(x, y) *((uint32_t *)((GPIO##x##_BASE - PERIPH_BASE + GPIO_IN_REGISTER_OFFSET)*32 + PERIPH_BB_BASE + y * 4))

PRT_WR(C, 13) = 1;
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории