С++, метапрограммирование и регистры микроконтроллера

Привет, Хабр!


Вот уже несколько лет все свои проекты для линейки микроконтроллеров stm32 я делаю на C++.


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


Во избежание лишних вопросов: я использую связку QtCreator+gcc+gdb+openocd. Как с ней работать, описано многократно, поэтому не буду на этом останавливаться, а вот о своих подходах к работе с микроконтроллерами расскажу подробнее.


На нижнем уровне проекта, как правило, находятся драйвера периферии. С них и начнём.


Я стараюсь, по возможности, не связываться с монстрами вроде SPL, HAL и, прости господи, CubeMX, разве что допиливая за деньги чужие проекты. Сама идеология фреймворка "как для больших машин", на мой взгляд, там порочна. Задуманные гибкими и удобными в использовании (хотя кто-то и поспорит с этим) функции превращаются в достаточно большой и неоптимальный машинный код, где многие лишние операции выполняются на микроконтроллере в рантайме. Да, современные контроллеры мощны, но компьютер, на котором мы собираем проект гораздо мощнее, поэтому пусть он всё и делает.


Я определил для себя основные требования к реализации управления периферией:


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

Наверное, только ленивый не придумывал свой велосипед для работы с полями регистров. Это и сдвиг плюс логические операции а-ля CMSIS, и всяческие макросы, скрывающие эти же операции в своих запутанных недрах и хтонь в стиле библиотек от ST. Одно время я даже работал через битовые поля, пока, наконец, не придумал решение, с которым и хочу ознакомить вас.


Здесь и далее в качестве примера буду использовать так любимый китайцами STM32f103 и его Flash access control register, который там один, поэтому пример будет простым и коротким.



Сконфигурируем flash средствами CMSIS для работы ядра на 72МГц:


FLASH->ACR = (FLASH->ACR &
              (~(FLASH_ACR_LATENCY_Msk
                 | FLASH_ACR_PRFTBE_Msk ))) // Плохо читаемое обнуление полей
             | FLASH_ACR_LATENCY_1          // Сразу не скажешь, сколько это и чего
             | FLASH_ACR_PRFTBE;

Не очень наглядно, не находите? Если через пару недель понадобится изменить код, придётся лезть в даташит и по-новому разбираться, где, что и как. Или писать везде подробные комментарии. Тем не менее, плюсом данного подхода является то, что сгенерированный машинный код очень компактный:


0x80001ec                  04 4a        ldr r2, [pc, #16]   ; (0x8000200 <main()+20>)
0x80001ee  <+    2>        13 68        ldr r3, [r2, #0]
0x80001f0  <+    4>        23 f0 17 03  bic.w   r3, r3, #23
0x80001f4  <+    8>        43 f0 12 03  orr.w   r3, r3, #18
0x80001f8  <+   12>        13 60        str r3, [r2, #0]

Как видим, всё по классике: чтение-модификация-запись. Можно ли получить такой код из более понятного программного текста?


Можно. В этом нам помогут шаблоны и метапрограммирование.


Прежде всего, необходимо сделать описание периферии. В моей концепции набор регистров каждого периферийного устройства представляет собой структуру. Для flash с его одним регистром это будет выглядеть так:


struct Regs {
    uint32_t ACR;   
    // Здесь могли бы быть ещё регистры
};

Для описания полей регистра используется следующее:


struct ACR {
    constexpr static uint8_t LATENCY[]{ 0, 3 };
    constexpr static uint8_t HLFCYA[]{ 3, 1 };
    constexpr static uint8_t PRFTBE[]{ 4, 1 };
    constexpr static uint8_t PRFTBS[]{ 5, 1 };
};  

Здесь первое число в каждом массиве — это смещение поля, а второе — его длина. Для автоматического получения подобных структур на Python на коленке был написан парсер SVD файлов.


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


constexpr static uint32_t base = 0x40022000; // Базовый адрес периферийного устройства
INLINE constexpr static volatile Regs* rg()
{
    return reinterpret_cast<volatile Regs*>(base);
}

Здесь base задаётся отдельным выражением, потому что в некоторых случаях (если, например, у нас несколько одинаковых устройств, например, таймеров) оно может быть и параметром шаблона. Об этом я расскажу в других статьях.


Макрос INLINE определён как


#ifndef INLINE
#define INLINE __attribute__((__always_inline__)) inline 
#endif

Это как раз тот случай, когда макрос оправдан. Запись становится более короткой, а в дальнейшем, при использовании компилятора отличного от gcc его можно будет условно переопределить.


Функция конфигурирования flash выглядит следующим образом:


INLINE static void setLatency(Flash::Latency latency, bool prefetchBufferEnable = false) 
{ 
    setRegister(rg()->ACR,
        ACR::LATENCY, static_cast<uint8_t>(latency),
        ACR::PRFTBE, prefetchBufferEnable
    ); 
}

На мой взгляд, всё достаточно удобочитаемо: как сам интерфейс функции, так и её код. Входное значение latency жёстко типизировано, попытка записать туда произвольное число приведёт к ошибке. Приведение типа static_cast<uint8_t>(latency) обязательно, иначе компилятор будет ругаться, несмотря на то, что Latency объявлено как:


enum class Latency : uint8_t {
    zeroWaitState = 0b000,
    oneWaitState = 0b001,
    twoWaitStates = 0b010
};

Вызов функции вида


Flash::setLatency(Flash::Latency::twoWaitStates, true);

компилируется в следующий машинный код:


0x80001ec                  04 4a        ldr r2, [pc, #16]   ; (0x8000200 <main()+20>)
0x80001ee  <+    2>        13 68        ldr r3, [r2, #0]
0x80001f0  <+    4>        23 f0 17 03  bic.w   r3, r3, #23
0x80001f4  <+    8>        43 f0 12 03  orr.w   r3, r3, #18
0x80001f8  <+    12>       13 60        str r3, [r2, #0]

Попробуйте найти отличие от реализации на CMSIS: всё происходит за один цикл чтения-модификации-записи, причём все константы вычисляются на этапе компиляции.


Какими средствами это достигается? Добро пожаловать под капот функции setRegister. Выглядит она следующим образом:


template<typename T, typename V, typename... Args>
INLINE constexpr static void setRegister(volatile uint32_t& reg,
                                         const T field,
                                         const V value,
                                         const Args... args)
{
    uint32_t mask = setMaskR(field, value, args...);
    uint32_t val = setBitsR(field, value, args...);

    reg = (reg & (~mask)) | val;
}

Ей передаётся ссылка на регистр (число uint32_t) и произвольное количество пар поле — значение. Далее при помощи вспомогательных функций конструируется маска для обнуления полей и число для записи в них.
Вспомогательные функции имеют вид:


template<typename V, typename T>
INLINE constexpr static uint32_t setBitsR(T field, V val)
{
    return (val << (field[0]));
}

template<typename V, typename T, typename... Args>
INLINE constexpr static uint32_t  setBitsR(T field, V val, Args... args)
{
    return (val << (field[0])) | setBitsR(args...);
}  

template<typename V, typename T>
INLINE constexpr static uint32_t setMaskR(T field, V val)
{
    return ((((1 << field[1]) - 1) << field[0]));
}

template<typename V, typename T, typename... Args>
INLINE constexpr static uint32_t  setMaskR(T field, V val, Args... args)
{
    return ((((1 << field[1]) - 1) << field[0])) | setMaskR(args...);
}

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


Зачем используются вспомогательные функции и почему сразу не написать рекурсивную функцию setRegister? Я сначала так и сделал и получил очень неприятный эффект. Предположим, нам нужно подёргать туда-сюда какой-то бит — типичная задача при программировании микроконтроллеров. Однако, компилятор умный, он видит, что мы последовательно меняем одно и то же значение, и как бы мы его не отмечали как volatile, он оставляет только операцию, выполняющую последнюю модификацию. К сожалению, код такой реализации у меня не сохранился, поэтому прошу поверить мне на слово или попробовать проверить это самостоятельно.


В моей текущей реализации этот эффект отсутствует. В этом можно убедиться:


Flash::setLatency(Flash::Latency::twoWaitStates, true);
Flash::setLatency(Flash::Latency::oneWaitState, true);

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


0x80001ec                  07 4a        ldr r2, [pc, #28]   ; (0x800020c <main()+32>)
0x80001ee  <+    2>        13 68        ldr r3, [r2, #0]
0x80001f0  <+    4>        23 f0 17 03  bic.w   r3, r3, #23
0x80001f4  <+    8>        43 f0 12 03  orr.w   r3, r3, #18
0x80001f8  <+   12>        13 60        str r3, [r2, #0]        ; пишем раз
0x80001fa  <+   14>        13 68        ldr r3, [r2, #0]
0x80001fc  <+   16>        23 f0 17 03  bic.w   r3, r3, #23
0x8000200  <+   20>        43 f0 11 03  orr.w   r3, r3, #17
0x8000204  <+   24>        13 60        str r3, [r2, #0]        ; пишем два

Функция чтения поля регистра достаточно тривиальна:


template<typename T>
INLINE constexpr static uint32_t getRegField(volatile uint32_t& reg,
                                             const T field)
{
    uint32_t mask = (((1 << field[1]) - 1) << field[0]); 
    return ((reg & mask) >> field[0]);
}

За один вызов функции считывается одно поле регистра.


Ниже — реализованная при её помощи сервисная функция:


INLINE static bool getLatencyPrefetch()
{
    return getRegField(rg()->ACR,
                       ACR::LATENCY,
                       ACR::PRFTBE);
}

Полностью класс для управления flash будет выглядеть следующим образом:


struct Flash {
    constexpr static uint32_t base = 0x40022000; //< Базовый адрес периферии

struct ACR {
    constexpr static uint8_t LATENCY[]{ 0, 3 };
    constexpr static uint8_t HLFCYA[]{ 3, 1 };
    constexpr static uint8_t PRFTBE[]{ 4, 1 };
    constexpr static uint8_t PRFTBS[]{ 5, 1 };
};    

enum class Latency : uint8_t {
    zeroWaitState = 0b000,
    oneWaitState = 0b001,
    twoWaitStates = 0b010
};

INLINE constexpr static volatile Regs* rg()
{
    return reinterpret_cast<volatile Regs*>(base);
}

INLINE static void setLatency(Flash::Latency latency, bool prefetchBufferEnable = false) 
{ 
    Utility::setRegister(rg()->ACR,
        ACR::LATENCY, static_cast<uint8_t>(latency),
        ACR::PRFTBE, prefetchBufferEnable
    ); 
}
INLINE static bool getLatencyPrefetch()
{
    return getRegField(rg()->ACR,
                       ACR::LATENCY,
                       ACR::PRFTBE);
}
};    

Работа с периферией извне осуществляется только через вызов методов классов. Можно было бы совсем закрыть всё лишнее, однако, исключительно для целей отладки иногда бывает полезно обратиться к полям класса напрямую. Поэтому всё определяется сознательностью разработчика.


Для работы с регистрами портов ввода-вывода я использую иной механизм, о нём будет в следующей статье.


Надеюсь, приведённая информация была вам полезна. В планах также рассказать о о реализации маленького лёгкого потока вывода в консоль, принципах построения шаблонов для работы со сторонней периферией и некоторых простых DSP-алгоритмах. Если это вам интересно — пишите в комментариях.

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 65

    +1
    … и собирать ошибки при ручном наборе всех этих
    twoWaitStates = 0b010

    Нет, так делать нельзя ни в коем случае. Либо писать обёртку над CMSIS, либо генератор классов из данных SVD, о чём упоминалось в habr.com/ru/post/459642
      0

      Интересная статья. Странно, что не видел её у себя в ленте.
      Никогда не получалось настолько глубоко погрузиться в шаблоны, всё время приходится искать компромис между сложностью реализации (читай — временем на разработку) и качеством получаемого результата. Автору — респект!


      С SVD файлами не всё так просто. Например, у того же ST в них есть некоторое количество ошибок, которые в неизменном виде кочуют в reference manual, видимо, его каркас генерируется автоматически. Нет там и признака R/W у поля регистра, и допустимых значений. Ручного труда по описанию периферии со всеми его ошибками не миновать, как ни крути.


      Плохо пока понимаю, как можно красиво написать обёртку над CMSIS. Слишком разные подходы.

    0
    В ARMCC 0b010 ошибка будет.
      0
      Она походу везде будет. Очень приятно за QT, что он умеет с таким представление работать.
        +2
        QT тут не при чем. Это gcc.
        0

        Пользуйтесь компиляторами, которые умеют C++14

          –2
          Пользуюсь головой, которая умеет всё и без С+14.
            0

            Можно и на голом асме вообще без С, не то что C++, но зачем?

              0
              Затем что бы можно было использовать не только под gcc, но и с armcc например. Хотя предвижу вопрос — зачем armcc когда есть gcc.
                0

                Правильно предвидите.


                Новые стандарты языка дают много удобства, как ни странно.

                  0
                  Я знаю, но не gcc единым живу.
                  0

                  На вкус и цвет…
                  Вот есть в Кейле отличный отладчик с просмотром содержимого регистров по полям. И монитор реального времени. Завидую. Однако, те возможности, что есть в gcc, для меня перевешивают всё это. Плюс возможность писать под любой операционкой, для меня это важно.

                    0

                    Можно использовать Clion от JetBrains с GDB и OpenOCD, подключить SVD и отладка будет тоже нормальная, с просмотром регистров по полям.

                      0
                      Я мимокрокодил но вот тут kuzulis вроде бы прикрутил отладчик keil к QtCreator (4.12 версия, по идее)
                        0
                        Просмотр регистров периферии уже есть в QtC 4.11, но только для GDB. Но кейловский отладчик прикручен в QtC 4.12 (пока только для симулятора и STLink для STM32, т.к оч много работы, не успеваю физически), но там пока без просмотра регистров (просмотр регистров с кейловским отладчиком уже закоммичен в QtC > 4.12).
                      +1

                      Зачем armcc, когда его поддержка прекращена в пользу armclang? :)

              +2
              И все эти выкрутасы нужны, чтобы включать лампочку по расписанию два раза в сутки…
                –1

                У меня эти выкрутасы работают при чтении 16 каналов 24-разрядного внешнего АЦП с вычислением амплитуд и фаз сигнала по нескольким частотам, параллельным чтением акселерометра, гироскопа и GPS, и передачей всей этой радости в реальном времени по USB HS.
                Есть за что бороться, не находите?

                  0
                  Просто уточнить, это всё на «так любимом китайцами STM32f103»?
                    0

                    Нет, крутится всё на stm32f429. stm32f103 для более лёгких применений. И как пример здесь. Но никто же не мешает так и с AVR работать. Благо там тоже gcc.

                +1
                С дма такой код будет выглядеть бесконечной строкой. А сам код дма переносить с одного мк на другой — не имеет ни малейшего смысла, потому как он железный. По этому гораздо проще написать статические структуры под конкретный мк, которые перепроцессор преобразует в одну запись.
                Чем вам CMSIS не угодил?
                void __attribute__ ((weak)) EXTI9_5_IRQHandler(void)
                {
                EXTI->PR = EXTI_PR_PR7;
                if (ls_tik == 0){
                EXTI->EMR &= ~EXTI_IMR_MR7;
                EXTI->IMR &= ~EXTI_IMR_MR7;
                EXTI->PR = EXTI_PR_PR7;
                DMA2_Stream1->NDTR = 51200;
                DMA2_Stream1->M0AR = (uint32_t) &image_buf[0];
                DMA2_Stream1->FCR = DMA_SxFCR_DMDIS | sDMA_SxFCR_FTH.full_FIFO_4_4;
                ls_tik = 1;
                DMA2_Stream1->CR = sdma_line.dma2.stream_1.ch7_TIM8_UP|
                sDMA_SxCR_DIR.memory_to_peripheral|
                sDMA_SxCR_MBURST.incremental_burst_of_4_beats|
                sDMA_SxCR_PBURST.single_transfer|
                _VAL2FLD( DMA_SxCR_PL, 3)|
                sDMA_SxCR_MSIZE.t32_bit|
                sDMA_SxCR_PSIZE.t16_bit|
                DMA_SxCR_MINC|
                DMA_SxCR_PINC|
                DMA_SxCR_TCIE|
                DMA_SxCR_EN;
                TIM8->CR1 |= TIM_CR1_CEN;
                };
                };
                  0
                  а для чего переопределять обработчик вектора повторно как слабый?
                  они ж итак стартапе описаны с этим атрибутом.

                  и да, маленькая очепятка у Вас:
                  EXTI->EMR &= ~EXTI_IMR_MR7;
                  EXTI->IMR &= ~EXTI_IMR_MR7;

                  явно должно быть:
                  EXTI->EMR &= ~EXTI_EMR_MR7;

                  не смертельно, так как маски в данном случае одинаковы, но внимательности это не отменяет.
                    0
                    mctMaks, слабая реализация функции совместно со слабым объявлением и одновременно с запретом на удаление адреса функции «KEEP» — автоматически разрешает её использование в изолированном виде. Точно так-же, как это требуется для прерываний, но теперь ничего изобретать уже не требуется.
                    То-есть можно написать реализацию прерывания в любом Си файле "*.c", без дополнительных объявлений и блокировок в хидере.
                    Что само по себе удобно и практично.
                      0
                      а что будет, если таких описаний будет несколько в разных файлах?

                      первый раз такое встречаю за свою практику, поэтому хочу больше подробностей знать)
                        0
                        Реализаций, объявление всех функций уже фиксировано.
                        Ничего страшного не будет, всё будет работать, но сами функции в прошивке будут не по порядку (1,2,3..) -а разбросаны по всей флешке.
                        И ещё, Си файлы (каталоги) включенные в проект, даже без подключенного хидера — будут автоматом использовать прерывания. По этому тут уже не обойтись условной компиляцией — файлы придётся удалять физически. Иначе либо закончится свободное место, или будет конфликт адресов. То-есть сделать из проекта помойку на десяток гигабайт чистого кода (на все случаи жизни) — уже не получится.
                          0
                          сложно как-то. Обработчик вектора все равно только один должен быть. Если есть необходимость использовать разные реализации в зависимости от типа проекта (ака солянка из исходников для сборки разных проектов), то проще настроить разные виды сборок, куда включены только нужные файлы.

                          В общем, либо не до конца понял эту идею, либо эта идея для слишком специфичных задач.

                          без включения хедера все равно не обойтись, банально по причине объявления в онном битовых масок для сброса\установки флагов.
                            0
                            В общем, либо не до конца понял эту идею

                            +1
                            У меня серьезные подозрения, что вышеприведённая конструкция работает только по совершенной случайности — потому что из двух weak'ов первым линкеру подсовывается правильный (не стандартный-пустой).

                  0
                  Меня всегда интересовало, почему регистры в микроконтроллерах до сих пор называют какими-то мозгодробительными аббревиатурами. Ну возможно в древние времена, когда 640К хватало всем, и IDE в современном понимании как таковых не было, это и имело смысл ради экономии места в памяти/на диске. Но современные микроконтроллеры — и все то же самое. Назвали бы нормально, длинным названием из нескольких слов, чтобы из названия было понятно что за регистр…
                    0
                    Регистры имеют названия только для IDE, для процессора и программы это просто ячейки ОЗУ в массиве памяти. Поэтому их называют максимально коротко, чтобы программисту приходилось нажимать меньше клавиш.
                      +1
                      Регистры имеют названия прежде всего для программиста и определены в официальной документации на микроконтроллер (Reference Manual). И именно эти названия первичны, а дальше они определяются в библиотеке (всяких HAL и CMSIS) как именованные ячейки ОЗУ, и в идеале они должны совпадать с тем что в документации. Хотя и это выполняется далеко не всегда:(
                      А экономить нажатия клавиш при современном автокомпилите — глупо.
                      0
                      Оно и так все понятно, для тех, кто в теме, а не для залётных. Более того, схожие биты регистров конфигурации и флагов — схоже называются.

                      Если ARM поднялась, можно сказать, из неоткуда, значит они все делают правильно, а дальше каждый сам подберет, что ему удобно: битовые поля из си, сборная солянка, шаблоны.

                      Шаблоны интересны тем, что на них можно сделать декранч регулируемого размера, но для этого надо вылезти из зоны комфорта обычного Си и начать думать по крестовски.
                      0

                      А как такое компилируется?


                      INLINE constexpr static volatile Regs* rg()
                      {
                          return reinterpret_cast<volatile Regs*>(base);
                      }

                      reinterpret_cast в constexpr запрещен же. Ну точнее, это не должно быть constexpr выражением, а значит, по стандарту — невозможно на этапе компиляции получить указатель на адрес регистра. В этом вся *опа то и зарылась…


                      An expression e is a core constant expression unless the evaluation of e, following the rules of the abstract
                      machine (4.6), would evaluate one of the following expressions:
                      (2.15) — a reinterpret_cast (8.2.10);

                      И скорее всего на другом компиляторе это не сработает.

                        0

                        Как ни странно, компилируется. Даже gcc 9.2.1 не выдаёт по этому поводу никаких предупреждений. Тот же Clang, который работает в QtCreator в качестве статического анализатора кода тоже молчит.


                        Однако, эта конструкция мне самому не нравится. На этапе отладки не всегда можно через неё получить значения регистров, gdb её плохо переваривает. Буду думать, чем заменить.

                          0

                          Она без оптимизации должна быть рантайме. При оптимизации компилятор волен оптимизировать все. Но в общем случае, все это дело будет выполняться в рантайме.
                          А потом по цепочке и все остальные функции тоже.
                          Для проверки, можете написать


                          constexpr volatile Regs* test = rg();
                          Получите ошибку, так как rg(); не может вычислить я на этапе компиляции.

                          0

                          reinterpret_cast можно написать в constexpr-функции, и в этом нет проблем, если все пути выполнения, проходящие через каст, не используются в constexpr-контекстах. То есть,


                          constexpr int stupid(int n)
                          {
                            if (n > 0)
                              return n;
                            reinterpret_cast<...>(...);
                            ...
                          }

                          Тогда int foo = stupid(-1); норм, и constexpr int foo = stupid(1); норм, а вот constexpr int foo = stupid(-1); не норм.


                          Другое дело, что ЕМНИП constexpr-функция, которая ни для каких аргументов не может быть constexpr, является то ли UB, то ли ill-formed/NDR. Что, в общем, лишний раз доказывает, что не бывает кода на плюсах без UB, причём в самых неожиданных местах.

                            0

                            Да, написать можно, но она перестаёт быть constexpr в этом случае, и в общем случае, указатель будет вычислен в рантайме и все остальное тогда тоже…

                              0

                              А она в исходном посте нигде не используется как constexpr, этот модификатор там условно для красоты, можно его убрать, и ничего не поменяется.


                              А насчёт «в этом случае» я как раз и написал тоже.

                                0

                                Она используется потом для передачи указателя, который якобы рассчитан на этапе компиляции в constexpr функцию, что может создать ложное впечатление о том, что функция getRegField() или setRegister() тоже constexpr.

                                  0

                                  Так она вполне constexpr, а вот их композиция — нет.

                                    0

                                    Ага. Я про это и говорю, поскольку по сути в параметр constexpr функции передаётся reinterpret_cast.

                          +1
                          Тема интересная, но мне кажется, абсолютно лучше писать тогда на Аде — получится что-то типа
                           FLASH_Periph.ACR.PRFTEN := 1;
                           FLASH_Periph.ACR.LATENCY := 2;
                          

                          И тогда уже сколько бы лет не прошло, все будут понимать одинаково, да и читается очень легко и транслируется так же как у вас…
                            0

                            Извините если вопросы наивны, я об этом языке имею самое поверхностное представление.


                            Получается, что Ада умеет оптимизировать работу с битовыми полями, и эти две операции будт выполнены как одно чтение-модификация-запись?


                            Если да, то как написать код, чтобы значения устанавливались последовательно, например, сначала необходимо включить периферию, а затем установить какое-то конфигурационное значение?

                              +2
                              Конечно, обычно же что-то включают, потом в цикле ждут, потом далее… Но, если так уж надо — Ада может все…
                              Для этого надо просто сообщить, как именно вы хотите организовать доступ — сразу ко всем битам (но менять только по одному) или можете к части бит (ну если это можно).
                              В первом случае надо написать
                              pragma Volatile_Full_Access(FLASH_Periph.ACR);
                              

                              во втором случае нужно, очевидно (это же атомарный доступ?)
                              pragma Atomic(FLASH_Periph.ACR);
                              


                              Вот и все… Ну, конечно, можно ввести свое собственное описание области памяти, тогда там нужно будет для нужного описания добавить with atomic или with volatile соответственно.
                            +5
                            Я наверно слишком старый.
                            Мне кажется, что
                            FLASH->ACR = (FLASH->ACR &
                            (~(FLASH_ACR_LATENCY_Msk
                            | FLASH_ACR_PRFTBE_Msk )))
                            | FLASH_ACR_LATENCY_1
                            | FLASH_ACR_PRFTBE;

                            это очень просто и очень наглядно.
                            Гораздо проще, чем темплейты и все эти пляски с бубном метапрограммированием.
                              0

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


                              Попробовал делать на шаблонах — понравилось, стало легче. В свою веру не обращаю, кому как удобнее.

                              0

                              Автор занимается бессмысленной работой, сначала для каждого мк объявляет кучу структур, потом прячет код внутри setLatency(), хотя можно было просто написать:


                              INLINE static void setLatency(Flash::Latency latency, bool prefetchBufferEnable = false)
                              {
                                  FLASH->ACR = FLASH->ACR & ~(FLASH_ACR_LATENCY | FLASH_ACR_PRFTEN) | 
                                      uint32_t(latency) | prefetchBufferEnable << FLASH_ACR_PRFTEN_Pos;
                              }

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

                                +1

                                Возможно. Не настаиваю на своём подходе, тем более у уважаемого lamerok всё гораздо лучше проработано в этом отношении.
                                Однако, это то, чем я пользуюсь в реальных проектах, мне так удобно. Возможно кому-то ещё пригодится, если нет — тоже не беда, по итогам обсуждения, спасибо сообществу, появилось множество новых идей, значит уже писал не зря.

                                  0
                                  INLINE static void setLatency(Flash::Latency latency, bool prefetchBufferEnable = false)
                                  {
                                  FLASH->ACR = FLASH->ACR & ~(FLASH_ACR_LATENCY | FLASH_ACR_PRFTEN) | 
                                  uint32_t(latency) | prefetchBufferEnable << FLASH_ACR_PRFTEN_Pos;
                                  }

                                  А вы уверены, что у вас в этом коде ошибки нет? :)

                                    0

                                    Какая тут ошибка? С приоритетами операций все нормально...

                                      0

                                      Это хорошо, что вы их помните, но ИМХО, для того, кто будет код этот смотреть, все не так очевидно. Все таки, ИМХО опять, что-то типа этого понятнее выглядит, хотя делает тоже самое:


                                        FLASH::ACRPack<
                                          FLASH::ACR::LATENCY::Six,
                                          FLASH::ACR::PRFTEN::Enable
                                        >::Set() ;

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

                                        –1
                                        А зачем в этот код смотреть тому, кто нетверд в приоритетах операций? Вы хотите, чтобы на C++ могла писать та самая кухарка, которая, по слухам, может управлять государством? И сколько реальных пользователей у Вашей шаблонной библиотеки?
                                        Как по мне — выигрыш в читаемости от Вашего шаблона минимален, веры ему не больше, чем «сишному» варианту, а значит надо куда-то лезть смотреть, что там enable_if'ами нагорожено. А там уж точно нечитаемо.
                                        Не поймите превратно, мне шаблонное метапрограммирование по душе. Писать их интересно. Но не все шаблоны одинаково полезны.
                                          0

                                          Насчет кухарки — да. Но дело в другом, дело в том, что в случае с С, много ручной работы, даже Flash::Latency — это кто задает этот енум? И потом, тут на С++, потому что, на сколько я помню, bool в Си нет. А раз нет bool, то в функцию можно передать, что угодно… Поэтому потенциально просто — тут ошибок можно наделать больше.
                                          Задача минимизировать количество ошибок программистом прикладного уровня. А тут огромное поле для этого.
                                          Шаблон не щаблон, это не важно — важно не дать делать пользователю вашей функции, то, чего делать не надо. Как миниму, там должны быть assert вставлены, на проверку входных параметров. А если это на С++ писано, то зачем так вообще писать, где явное преобразование из bool в uint32_t, например? Тут какой-то симбиоз Си и С++. Я даже не уверен даже, что там все по стандарту сделано С++. Статический анализатор выдает 25 предупреждений на одной этой строчке. Такой код никто не пропустит в продакшен.

                                            0

                                            Это написано на C++, bool неявно приводится к int, приводить его к uint32_t в данном случае не обязательно, т.к. даже несмотря на возможное смешение знаковых и беззнаковых типов бинарно результат все равно будет одинаковым. У меня обычно параноидальные настройки ворнингов не выставлены, но сейчас добавил для Gcc -Wall -Wextra -Wpedantic и получил только предупреждение касательно скобок, причем если их добавить, то их выводит серым цветом, как необязательные :) VS и Resharper тоже молчат, хотя там часть предупреждений заблокирована, так что даже не знаю что нужно сделать чтобы на одной строке получить 25 предупреждений...

                                              0

                                              Я особо не разбирался, вот скриншот



                                              А если проверить PC Lint, то тот вообще наверное с ума сойдет :)

                                                0

                                                Я взял функцию из статьи и по-быстрому переделал, естественно имена старался не менять, потому если анализатору не нравится сбивающее с толку имя, в этом не моя вина :) Далее, все предупреждения где встречаются PERIPH_BASE и т.д., которых нет в моем коде, а так же все ворнинги с приведением кроме одного моего "old style cast" — это все предупреждения для стандартных хедеров. Причем местами не понятно что оно в принципе хочет, например, у меня FLASH_ACR_LATENCY_Msk задефайнен как (0x7UL << FLASH_ACR_LATENCY_Pos), а в предупреждении речь идет про underlying type unsigned 8-bit int… И где оно 8 бит увидело? Ну ладно, я то как-то переживу, а многие использовали CMSIS даже не подозревая, что в продакшен с ним код не пустят :)

                                                  0
                                                  Ну ладно, я то как-то переживу, а многие использовали CMSIS даже не подозревая, что в продакшен с ним код не пустят :)

                                                  Я имею ввиду для надежных промышленных систем, где код проверяют и сертифицируют, ну или медицина, например. Вообще, у ST есть специальная версия Cube и HAL с сертификатом, но так просто вы её не получите, только через NDA, там подобные вещи убраны, упрощены и пофиксины.
                                                  st cube SIL3


                                                  FLASH_ACR_LATENCY_Msk задефайнен как (0x7UL << FLASH_ACR_LATENCY_Pos)

                                                  у меня так вот задефайнено, без L… У вас какая версия CMSIS и для какого процессора? :)


                                                  #define FLASH_ACR_LATENCY_Pos          (0U)                                    
                                                  #define FLASH_ACR_LATENCY_Msk          (0xFU << FLASH_ACR_LATENCY_Pos)         /*!< 0x0000000F */
                                                    0

                                                    Я проверял на STM32G0, а к нему старых хедеров не может быть в принципе, но и для всех остальных серий стоит UL, кроме древних хедеров от SPL:


                                                    #define  FLASH_ACR_LATENCY                   ((uint8_t)0x03) 

                                                    Там еще и многие регистры объявлены 16-ти битными, но это было давно, сейчас скачал для проверки ST-й пак STM32CubeF4 и там все в точности как у меня в VisualGDB, никаких 8-ми битных констант в хедерах от производителя уже нет много лет.

                                  0
                                  Ну и мне еще не понятно, почему же в обсуждении был обойден простой и понятный способ делать такие вещи, типа такого:
                                  typdef struct {
                                    uint8_t pole1: 1;
                                    uint8_t pole2: 2;
                                    uint8_t pole3: 1;
                                    uint8_t pole4: 2;
                                  } my_reg;
                                  ...
                                   ((my_reg*)(X))->pole1 = 0;
                                   ((my_reg*)(X))->pole2 |= 1;
                                  ...
                                  


                                  Тут все понятно и наглядно, компилятор все так же оптимально транслирует… зачем С++?
                                    0
                                    На вкус и цвет,…
                                    Как по мне, в данном подходе есть необходимость помнить какие биты надо ставить и как они переопределены программистом.

                                    Пользовался подобным описание для регистров внешних чипов, тот же акселерометр например. Не очень удобно оказалось. Особенно когда коллега перепутал порядок бит при описании регистров…
                                      0
                                      Поблемы возможны производительностью с… (С) Магистр Йода
                                        0
                                        Вы не поверите… но трансляция показала, что все одинаково — что C++ что C что Ada — все транслироволось в те же 2 команды ассемблера. Ну это и неудивительно, на самом деле, gcc вполне хороший…
                                          0
                                          Вроде по классике либо load+mask+shift либо load+mask+shift+or+store, и так для каждого поля. Возможно, компиляция шагнула далеко, я могу отстать от тенденций.
                                          И еще: прямо запись в несколько битовых полей объединяет в две ассемблерные инструкции? Или Вы про запись в одно поле?
                                            0

                                            У меня, когда я работал через битовые поля, каждое обращение к полю компилировалось в отдельную операцию чтение-модификация-запись. Собирал тогда GCC 7

                                              0

                                              Так и будет, если все поля, как и положено для регистров, объявлены как volatile, потому что порядок установки может иметь значение.

                                    Only users with full accounts can post comments. Log in, please.