Комментарии 38
А по теме в общем, я могу ошибаться, но вроде как стандарт языка позволяет компилятору конвертировать constexpr функцию в run time, если ему кажется это правильным. Поэтому я всегда очень осторожно к ним отношусь и проверяю каждый случай применения.
Можно еще static_assert вставить внутрь функции на проверку входных аргументов, если фукнция будет не constexpr, т. Е кто то попытается рантайм передать значения. Не посчитаннве на этапе компиляции, компилятор руганется.
static_assert
вы можете сделать только «снаружи» — а там достаточно присвоить значение constexpr
-переменной, чтобы гарантировать, что всё будет вычислено в компайл-тайме. Даже в -O0. Собственно вот. Сравните код bar
и baz
…godbolt — ваш друг а таких вещах.
Однако я не считаю верным утверждение, что основная особенность — атомарность операции. Я думаю экономия памяти по значимости не уступает ей.
На запрещение/разрешение прерываний уходит более чем достаточно как байтов кода, так и таков.
Думаю вам стоит поправить/доработать статью.
По теме байтов, вы приводите ссылку на другую версию ядра. В cortex-m3 это всего-лишь одна инструкция CPSIE/ CPSID.
Но дело то даже и не в этом. На счёт той же атомарности, в STM32, к примеру, почти все биты, что могут поменяться из вне, вынесены в отдельный Read-Only регистр (ANY_PERIPH)->SR. Если подобные биты есть в других регистрах, то обычно сбросить подобные флаги можно только записью единицы, что не нарушит логику программы даже при неатомарной операции.
Видимо есть недопонимание "атомарности".
В случае управления "выходами" GPIO речь не о memory barries и/или memory ordering, а о потенциальном переключении процессора на другую задачу и/или обработчик прерываний, которые могут обращаться к тому-же GPIO. Соответственно будет прелестный heisenbug, если переключение произойдет между LDR
и STR
.
Кроме этого, CPSIE
не всегда подходит, например если прерывания были запрещены выше по стеку вызовов для более широкого контекста. Соответственно, нередко перед CPSID приходится сохранять маску, а вместо CPSIE ёё восстанавливать.
В случае управления «выходами» 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 только ради этого придуман.
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'а.
Предлагаю дискуссию завершить, а то по третьему кругу пошли. Думаю все всё уже поняли.
Небольшой примерчик для глобальных флагов — forum.easyelectronics.ru/viewtopic.php?f=35&t=22572
в С++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;
}
Cortex-M Processor Family». Думая о том, какие возможности это дает, добавил макросы, похожие на представленные в статье, в свои библиотеки. В результате, за два года ими никто не пользовался, включая меня(((
У этого конечно есть применение по мимо настройки периферии, например можно дать какому-то классу «указатель на бит», управляющий уровнем GPIO. Но как оказалось, в работе этого не достаточно.
Вообще у ядра есть более крутые возможности вроде:
- Перенос таблицы прерываний в ОЗУ (начиная с cortex-M0+). Очень удобно, когда куски кода сами назначают себе прерывание.
- Программные прерывания и вызов прерываний командой — это позволяет, например, писать тесты.
- Математика с насыщением(Saturated Maths Instructions), которая очень хороша при работе с ПИД-регуляторами или цифровыми фильтрами.
Разумеется есть куча тругих, не менее полезных возможностей, вроде проверки указателя на валидность.
Видимо планировалось массовое производство ПЛК на основе Cortex-M.
__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;
Аппаратный bit banding CortexM3/M4(ARM), архитектура ядра, ассемблер, С/C++14 и капля метапрограммирования