image
Рис. взят с сайта www.extremetech.com/wp-content/uploads/2016/07/MegaProcessor-Feature.jpg

Всем доброго здравия!

В прошлой статье я рассмотрел вопрос о проблеме доступа к регистрам микроконтроллера с ядром CortexM на языке С++ и показал простые варианты решения части проблем.

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

Всех кого заинтересовал, добро пожаловать под кат. Кода будет много.

Введение


В статье C++ Hardware Register Access Redux, Ken Smith показал, как безопасно и эффективно работать с регистрами и даже показал это на примере github.com/kensmith/cppmmio.
Затем несколько людей развивали эту идею, например, Niklas Hauser сделал замечательный обзор и предложил еще несколько способов для того, чтобы безопасно обращаться к регистрам.

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

Сегодня каждый производитель ARM микроконтроллера поставляет описание всех регистров в формате SVD. Из этих описаний можно сгенерировать заголовочные файлы, поэтому открывается возможность создавать не простое описание регистров, а более сложное, но зато, позволяющее повысить надежность вашего кода. И замечательно, что выходной файл может быть на любом языке, С, С++, или даже D

Но давайте по порядку, что такое вообще безопасный доступ к регистрам, и для чего это вообще нужно. Возможное объяснение можно показать на простых синтетических, скорее всего маловероятных, но вполне возможных примерах:

int main(void)
{
  // Хотели включить тактирование на порте GPIOA   
  //опечатка, должен быть регистр AHB1ENR 
   RCC->APB1ENR |= RCC_AHB1ENR_GPIOAEN ; 

  //хотели установить только один бит, но обнулили все биты регистра 
  RCC->AHB1ENR = RCC_AHB1ENR_GPIOAEN; 

  //Неправильно, таймер TIM1 подключен к шине APB2 
  RCC->APB1ENR  |=  RCC_APB1ENR_TIM2EN | RCC_APB2ENR_TIM1EN;
 
  //Видимо кто-то решил, что можно считать состояние порта из этого регистра.      
  auto result = GPIOA->BSRR ; 
  if (result & GPIO_BSRR_BS1)  
  {
     //do something
  }
 
  //Кому-то платят за количество строк кода. Так ведь можно...
  GPIOA->IDR = GPIO_IDR_ID5 ;   
}

Все эти случаи возможны на практике и я точно наблюдал что-то подобное у своих студентов. Было бы замечательно, если можно было бы запретить совершать подобные ошибки.

Еще мне кажется, что намного приятнее, когда код выглядит аккуратным и не нуждающимся в комментариях. Например, даже если вы хорошо знаете микроконтроллер STM32F411, то не всегда можно понять, что происходит в таком коде:

int main()
{
  uint32 temp = GPIOA->OSPEEDR ;
  temp &=~ GPIO_OSPEEDR_OSPEED0_Msk ; 
  temp = (GPIO_OSPEEDR_OSPEED0_0 | GPIO_OSPEEDR_OSPEED0_1) ;
  GPIOA->OSPEEDR =  temp;
}

Без комментариев тут не обойтись. Код устанавливает частоту работы порта GPIOA.0 на максимум (уточнение от mctMaks: на самом деле этот параметр влияет на время нарастания фронта (то есть его крутизну), и означает, что порт может нормально обрабатывать цифровой сигнал на заданной(VeryLow\Low\Medium\High) частоте).

Давайте попробуем избавиться от этих недочетов.

Абстракция регистра


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

Регистр имеет адрес, длину или размер, режим доступа: в некоторые регистры можно писать, некоторые только читать, а большинство и читать и писать.

Кроме того, регистр можно представить как набор полей. Поле может состоять из одного бита или из нескольких битов и находится в любом месте регистра.

Поэтому для нас важны следующие характеристики поля: длина или размер(width или size), смещение относительно начала регистра (offset) и значение.

Значения поля есть пространство всех возможных величин, которые может принимать поле и оно зависит от длины поля. Т.е. если поле имеет длину 2, то существует 4 возможные значения поля (0,1,2,3). Так же как у регистра, у полей и значений полей есть режим доступа (чтение, записать, чтение и запись)

Чтобы было нагляднее, возьмем регистр CR1 Таймера TIM1 у микроконтроллера STM32F411. Схематично он выглядит вот так:

image

  • Бит 0 CEN: Включить счетчик
    0: Счетчик включен: Disable
    1: Счетчик выключен: Enable
  • Бит 1 UDIS: Включение/Выключение события UEV
    0: Событие UEV включено: Enable
    1: Событие UEV выключено: Disable
  • Бит 2 URS: выбор источников генерирования события UEV
    0: UEV генерируется при переполнении или при установке бита UG: Any
    1: UEV генерируется только при переполнении: Overflow
  • Бит 3 OPM: Режим одноразового срабатывания
    0: Таймер продолжает считать дальше после события UEV: ContinueAfterUEV
    1: Таймер останавливается после события UEV: StopAfterUEV
  • Бит 4 DIR: Направление счета
    0: Прямой счет: Upcounter
    1: Обратный счет: Downcounter
  • Бит 6:5 CMS: Режим выравнивания
    0: Режим выравнивания 0: CenterAlignedMode0
    1: Режим выравнивания 1: CenterAlignedMode1
    2: Режим выравнивания 2: CenterAlignedMode2
    3: Режим выравнивания 3: CenterAlignedMode3
  • Бит 7 APRE: Режима предзагрузки для регистра ARR
    0: Регистр TIMx_ARR не буферизируется: ARRNotBuffered
    1: Регистр TIMx_ARR не буферизируется: ARRBuffered
  • Бит 8:9 CKD: Делитель частоты тактового сигнала
    0: tDTS=tCK_INT: ClockDevidedBy1
    1: tDTS=2*tCK_INT: ClockDevidedBy2
    2: tDTS=4*tCK_INT: ClockDevidedBy4
    3: Reserved: Reserved

Здесь, например, CEN — это поле размером 1 бит имеющее смещение 0 относительно начала регистра. А Enable(1) и Disable(0) это его возможные значения.

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

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

image

Помимо классов, нам важно еще то, что регистры и отдельные поля имеют определенные свойства, у регистра есть адрес, размер, режим доступа(только для чтения, записи или для того и другого).
У поля есть размер, смещение и также режим доступа. Кроме того, поле должно содержать ссылку на регистр, которому оно принадлежит.

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

Поэтому в более детальном варианте наша абстракция будет выглядеть так:



Помимо атрибутов у нашей абстракции должны быть методы модификации и доступа. Для простоты ограничимся только методами установки/записи и чтения.

image

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

System View Description (SVD) Файл


Формат описания системного представления CMSIS (CMSIS-SVD) — это формальное описание регистров микроконтроллеров на базе процессора ARM Cortex-M. Информация, содержащаяся в описаниях системного представления, практически соответствует данным в справочных руководствах по устройствам. Описание регистров в таком файле может содержать как высокоуровневую информацию так и назначения отдельного бита поля в регистре.

Схематично уровни детализации информации в таком файле можно описать следующей схемой, взятой на сайте Keil:

image

Файлы SVD с описаниями поставляются производителями и используются при отладке для отображения информации о микроконтроллере и регистрах. Например, IAR использует их для отображения информации в панели View->Registers. Сами файлы лежат в папке, Program Files (x86)\IAR Systems\Embedded Workbench 8.3\arm\config\debugger.

Clion от JetBrains также использует svd файлы для отображения информации о регистрах при отладке.

Вы всегда можете скачать описания с сайтов производителя. Здесь можно взять SVD файл для микроконтроллера STM32F411

В общем, SVD формат это стандарт, который поддерживают производители. Давайте разберемся, что представляют из себя уровни описания в SVD.

Всего выделяют 5 уровней, Уровень устройства, уровень микроконтроллера, уровень регистров, уровень полей, уровень перечисляемых значений.

  • Уровень устройства: верхний уровень описания системного представления — это устройство. На этом уровне описываются свойства относящаяся к устройству в целом. Например, имя устройства, описание или версия. Минимальный адресуемый блок, а также разрядность шины данных. Значения по умолчанию для атрибутов регистра, таких как размер регистра, значение сброса и разрешения доступа, могут быть установлены для всего устройства на этом уровне и неявно наследуются нижними уровнями описания.
  • Уровень микроконтроллера: раздел CPU описывает ядро микроконтроллера и его особенности. Этот раздел является обязательным, если файл SVD используется для создания файла заголовка устройства.
  • Уровень периферийных устройств: периферийное устройство-это именованная коллекция регистров. Периферийное устройство сопоставляется с определенным базовым адресом в адресном пространстве устройства.
  • Уровень регистров: регистр — это именованный программируемый ресурс, принадлежащий периферийному устройству. Регистры сопоставляются с определенным адресом в адресном пространстве устройства. Адрес указывается относительно базового периферийного адреса. Также для регистра указывается режим доступа (чтения/записи).
  • Уровень полей: как уже сказано выше, регистры могут быть разделены на куски битов различной функциональности — поля. Данный уровень содержит имена полей, которые в пределах одного регистра уникальны, их размер, смещения относительно начала регистра, а также режим доступа.
  • Уровень перечисляемых значений полей: по сути это именованные значения поля, которые можно использовать для удобства в языках С, С++, D и так далее.

По сути SVD файлы являются обычными xml файлами с полным описанием системы. Существуют конвертеры svd файла в Си код, например здесь, которые генерируют удобные для использования Си заголовочники и структуры для каждой периферии и регистра.

Также существует парсер SVD файлов cmsis-svd, написанный на Phyton, который делает что-то типа десериализации данных из файла в объекты классов на Phython, которые затем удобно использовать в вашей программе генерации кода.

Пример описания регистра микроконтроллера STM32F411 можно посмотреть под спойлером:

Пример регистра CR1 таймера TIM1
<peripheral>
      <name>TIM1</name>
      <description>Advanced-timers</description>
      <groupName>TIM</groupName>
      <baseAddress>0x40010000</baseAddress>
      <addressBlock>
        <offset>0x0</offset>
        <size>0x400</size>
        <usage>registers</usage>
      </addressBlock>
      <registers>
        <register>
          <name>CR1</name>
          <displayName>CR1</displayName>
          <description>control register 1</description>
          <addressOffset>0x0</addressOffset>
          <size>0x20</size>
          <access>read-write</access>
          <resetValue>0x0000</resetValue>
          <fields>
            <field>
              <name>CKD</name>
              <description>Clock division</description>
              <bitOffset>8</bitOffset>
              <bitWidth>2</bitWidth>
            </field>
            <field>
              <name>ARPE</name>
              <description>Auto-reload preload enable</description>
              <bitOffset>7</bitOffset>
              <bitWidth>1</bitWidth>
            </field>
            <field>
              <name>CMS</name>
              <description>Center-aligned mode
              selection</description>
              <bitOffset>5</bitOffset>
              <bitWidth>2</bitWidth>
            </field>
            <field>
              <name>DIR</name>
              <description>Direction</description>
              <bitOffset>4</bitOffset>
              <bitWidth>1</bitWidth>
            </field>
            <field>
              <name>OPM</name>
              <description>One-pulse mode</description>
              <bitOffset>3</bitOffset>
              <bitWidth>1</bitWidth>
            </field>
            <field>
              <name>URS</name>
              <description>Update request source</description>
              <bitOffset>2</bitOffset>
              <bitWidth>1</bitWidth>
            </field>
            <field>
              <name>UDIS</name>
              <description>Update disable</description>
              <bitOffset>1</bitOffset>
              <bitWidth>1</bitWidth>
            </field>
            <field>
              <name>CEN</name>
              <description>Counter enable</description>
              <bitOffset>0</bitOffset>
              <bitWidth>1</bitWidth>
            </field>
          </fields>
        </register>
        <register>


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

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

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

Реализация


Регистр


Теперь когда сделана абстракция регистра и у нас есть описание регистров в виде svd от производителей идеально подходящее под эту абстракцию, можно перейти непосредственно к реализации.

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

  if (TIM1::CR1::CKD::DividedBy2::IsSet())
  {
     TIM1::ARR::Set(10_ms) ;
     TIM1::CR1::CEN::Enable::Set() ;
  }

Вспомним, что для того, чтобы обратиться по целочисленному адресу регистра, нужно использовать reinterpret_cast:

*reinterpret_cast<volatile uint32_t *>(0x40010000) = (1U << 5U) ;

Класс регистр уже был описан выше, он должен иметь адрес, размер и режим доступа, а также два метода Get() и Set():

//Базовый класс для работы с регистром
template<uint32_t address, size_t size, typename AccessMode>
struct RegisterBase
{
  static constexpr auto Addr = address ;
  using Type = typename RegisterType<size>::Type ;
  
  //Метод Set будет работать только для регистров, 
  //в которые можно записать значение
  __forceinline template<typename T = AccessMode,
     class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>>
  inline static void Set(Type value)
  {
    *reinterpret_cast<volatile Type *>(address) = value ;
  }
  
  //Метод Get возвращает целое значение регистра, 
  //будет работать только для регистров, которые можно считать
  __forceinline template<typename T = AccessMode,
     class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>>
  inline static Type Get()
  {
    return *reinterpret_cast<volatile Type *>(address) ;
  }
} ;

В параметры шаблона мы передаем адрес, длину регистра и режим доступа (это тоже класс). С помощью механизма SFINAE, а именно метафункции enable_if будем «выкидывать» функции доступа Set() или Get() для регистров, которые не должны их поддерживать. Например, если регистр только для чтения, то в параметр шаблона мы передадим ему тип ReadMode, enable_if проверит, является ли доступ наследником ReadMode и если нет, то создаст контролируемую ошибку (тип T не сможет быть выведен), и компилятор не станет включать метод Set() для такого регистра. Тоже самое и для регистра предназначенного только для записи.

Для контроля доступа будем использовать классы:

//Режим доступа к регистрам
struct WriteMode {}; 
struct ReadMode {}; 
struct ReadWriteMode: public WriteMode, public ReadMode {};

Регистры бывают разного размера: 8, 16, 32, 64 бита для каждого из них зададим свой тип:

Тип регистров в зависимости от размера
template <uint32_t size>
struct RegisterType {} ;

template<>
struct RegisterType<8>
{
  using Type = uint8_t ;
} ;

template<>
struct RegisterType<16>
{
  using Type = uint16_t ;
} ;

template<>
struct RegisterType<32>
{
  using Type = uint32_t ;
} ;

template<>
struct RegisterType<64>
{
  using Type = uint64_t ;
} ;


После этого для таймера TIM1 можно определить регистр CR1 и, например, регистр EGR вот таким способом:

struct TIM1
{
   struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode>
   {
   }
   struct EGR : public RegisterBase<0x40010014, 32, WriteMode>
   {
   }
}
int main()
{
  TIM1::CR1::Set(10) ;
  auto reg = TIM1::CR1::Get() ;

 //ошибка компиляции, регистр только для записи
  reg = TIM1::EGR::Get() 
}

Поскольку компилятор выводит метод Get() только для регистров у которых режим доступа наследуется от ReadMode, а методы Set() для регистров у которых режим доступа наследуется от WriteMode, то, в случае неверного использования методов доступа, вы получите ошибку на этапе компиляции. А если будете использовать современные средства разработки, типа Clion, то даже еще на этапе кодирования увидите предупреждение от анализатора кода:

image

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

Поля


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

//Базовый класс для работы с битовыми полями регистров
template<typename Reg, size_t offset, size_t size, typename AccessMode>
struct RegisterField
{
  using RegType = typename Reg::Type ;
  using Register = Reg ;
  static constexpr RegType Offset = offset ;
  static constexpr RegType Size = size ;
  using Access = AccessMode ;

  template<typename T = AccessMode,
        class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>>
  static void Set(RegType value)
  {
    assert(value < (1U << size)) ;
    //CriticalSection cs ; 

    //Сохраняем текущее значение регистра
    RegType newRegValue = *reinterpret_cast<RegType *>(Register::Address) ; 

    //Вначале нужно очистить старое значение битового поля
    newRegValue &= ~ (((1U << size) - 1U) << offset); 

    // Затем установить новое
    newRegValue |= (value << offset) ; 
    
    //И записать новое значение в регистр
    *reinterpret_cast<RegType *>(Reg::Address) = newRegValue ; 
  }
  
  __forceinline template<typename T = AccessMode,
        class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>>
  inline static RegType Get()
  {
    
    return ((*reinterpret_cast<RegType *>(Reg::Address)) &  
            (((1U << size) - 1U) << offset)) >> offset ; 
  }
};

После этого уже возможно делать следующие вещи:

struct TIM1
{
   struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode>
   {
     using CKD = RegisterField<TIM1::CR1, 8, 2, ReadWriteMode> ;
     using ARPE = RegisterField<TIM1::CR1, 7, 1, ReadWriteMode> ;
     using CMS = RegisterField<TIM1::CR1, 5, 2, ReadWriteMode> ;
     using DIR = RegisterField<TIM1::CR1, 4, 1, ReadWriteMode> ;
     using OPM = RegisterField<TIM1::CR1, 3, 1, ReadWriteMode> ;
     using URS = RegisterField<TIM1::CR1, 2, 1, ReadWriteMode> ;
     using UDIS = RegisterField<TIM1::CR1, 1, 1, ReadWriteMode> ;
     using CEN = RegisterField<TIM1::CR1, 0, 1, ReadWriteMode> ;
   }
}

int main()
{
  // в регистре CR1 бит 9 установится в 1, бит 8 в 0
  TIM1::CR1::CKD::Set(2U) ; 
  auto reg = TIM1::CR1::CEN::Get() ;
}

Хотя в целом всё выглядит неплохо, но все еще не совсем понятно, что значит TIM1::CR1::CKD::Set(2), что означает магическая двойка переданная в функцию Set()? И что означает число, которое возвратил метод TIM1::CR1::CEN::Get()?

Плавно переходим к значениям полей.

Значение полей


Абстракция значения поля это по сути тоже поле, но способное принимать только одно состояние. К абстракции поля добавляется атрибуты — собственно значение и ссылка на поле. Метод Set() установки значения поля, идентичен методу Set() установки поля, за тем исключением, что само значение не нужно передавать в метод, оно заранее известно, его просто надо установить. А вот метод Get() не имеет никакого смысла, вместо него него лучше проверить, установлено ли это значение или нет, заменим этот метод на метод IsSet().

//Базовый класс для работы с битовыми полями регистров
template<typename Field, typename Field::Register::Type value>
struct FieldValueBase
{
  using RegType = typename Field::Register::Type ;

  template<typename T = typename Field::Access,
        class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>>
  static void Set()
  {
    RegType newRegValue = *reinterpret_cast<RegType *>(Field::Register::Address) ; 
    newRegValue &= ~ (((1U << Field::Size) - 1U) << Field::Offset); 
    newRegValue |= (value << Field::Offset) ; 
    *reinterpret_cast<RegType *>(Field::Register::Address) = newRegValue ; 
  }

  __forceinline template<typename T = typename Field::Access,
        class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>>
  inline static bool IsSet()
  {
    return ((*reinterpret_cast<RegType *>(Field::Register::Address)) &
           static_cast<RegType>(((1U << Field::Size) - 1U) << Field::Offset)) ==
           (value << Field::Offset) ;
  }
};

Поле регистра теперь можно описать набором его значений:

Значения полей регистра CR1 таймера TIM1
template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_CKD_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using DividedBy1 = FieldValue<TIM_CR_CKD_Values, 0U> ;
  using DividedBy2 = FieldValue<TIM_CR_CKD_Values, 1U> ;
  using DividedBy4 = FieldValue<TIM_CR_CKD_Values, 2U> ;
  using Reserved = FieldValue<TIM_CR_CKD_Values, 3U> ;
} ;

template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_ARPE_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using ARRNotBuffered = FieldValue<TIM_CR_ARPE_Values, 0U> ;
  using ARRBuffered = FieldValue<TIM_CR_ARPE_Values, 1U> ;
} ;

template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_CMS_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using CenterAlignedMode0 = FieldValue<TIM_CR_CMS_Values, 0U> ;
  using CenterAlignedMode1 = FieldValue<TIM_CR_CMS_Values, 1U> ;
  using CenterAlignedMode2 = FieldValue<TIM_CR_CMS_Values, 2U> ;
  using CenterAlignedMode3 = FieldValue<TIM_CR_CMS_Values, 3U> ;
} ;

template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_DIR_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using Upcounter = FieldValue<TIM_CR_DIR_Values, 0U> ;
  using Downcounter = FieldValue<TIM_CR_DIR_Values, 1U> ;
} ;

template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_OPM_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using ContinueAfterUEV = FieldValue<TIM_CR_OPM_Values, 0U> ;
  using StopAfterUEV = FieldValue<TIM_CR_OPM_Values, 1U> ;
} ;

template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_URS_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using Any = FieldValue<TIM_CR_URS_Values, 0U> ;
  using Overflow = FieldValue<TIM_CR_URS_Values, 1U> ;
} ;

template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_UDIS_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using Enable  = FieldValue<TIM_CR_UDIS_Values, 0U> ;
  using Disable = FieldValue<TIM_CR_UDIS_Values, 1U> ;
} ;

template <typename Reg, size_t offset, size_t size, typename AccessMode> 
struct TIM_CR_CEN_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using Disable = FieldValue<TIM_CR_CEN_Values, 0U> ;
  using Enable = FieldValue<TIM_CR_CEN_Values, 1U> ;
} ;


Тогда сам регистр CR1 уже будет описан следующим образом:

struct TIM1
{
  struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode>
  {
    using CKD = TIM_CR1_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode> ;
    using ARPE = TIM_CR1_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode> ;
    using CMS = TIM_CR1_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode> ;
    using DIR = TIM_CR1_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode> ;
    using OPM = TIM_CR1_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode> ;
    using URS = TIM_CR1_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode> ;
    using UDIS = TIM_CR1_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode> ;
    using CEN = TIM_CR1_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode> ;
  } ;
}

Появилась возможность устанавливать и считывать непосредственно значение поля регистра: Например, если необходимо включить таймер на счет, достаточно вызвать метод Set() у значения Enable поля CEN регистра CR1 таймера TIM1: TIM1::CR1::CEN::Enable::Set() ;. В коде это будет выглядеть так:

int main()
{
  if (TIM1::CR1::CKD::DividedBy2::IsSet())
  {
    TIM1::ARR::Set(100U) ;
    TIM1::CR1::CEN::Enable::Set() ;
  }
}

Для сравнения, тоже самое и использованием Си заголовочника:
int main()
{
  if((TIM1->CR1 & TIM_CR1_CKD_Msk) == TIM_CR1_CKD_0)
  {
     TIM1->ARR = 100U ;
     regValue = TIM1->CR1 ;
     regValue &=~(TIM_CR1_CEN_Msk) ;
     regValue |= TIM_CR1_CEN ;
     TIM1->CR1 = regValue ;
  }
}


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

Однако все еще остается один недочет, невозможно одновременно поставить несколько значений полей в регистре. Представим, что надо сделать так:

int main()
{
   uint32_t regValue = TIM1->CR1 ;
   regValue &=~(TIM_CR1_CKD_Msk | TIM_CR1_DIR) ;
   regValue |= (TIM_CR1_CEN | TIM_CR1_CKD_0 | TIM_CR1_CKD_0) ;
   TIM1->CR1 = regValue ;
}

Для этого нам нужно либо у регистра сделать метод Set(...) с переменным числом аргументов, либо попытаться указать значения полей, которые нужно установить в шаблоне. Т.е. реализовать один из следующих вариантов:

int main()
{
  //Вариант 1, переменное количество аргументов в функции Set()
  TIM1::CR1::Set(TIM1::CR1::DIR::Upcounter, 
                TIM1::CR1::CKD::DividedBy4, 
                TIM1::CR1::CEN::Enable) ;

   //Вариант 2, параметры передаются в шаблон
   TIM1::CR1<TIM1::CR1::DIR::Upcounter, 
                TIM1::CR1::CKD::DividedBy4, 
                TIM1::CR1::CEN::Enable>::Set() ;
}

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

Воспользуемся шаблоном с переменным количеством аргументов. Список значений передадим как список типов:

//Класс для работы с регистром, можно передавать список значений полей для установки и проверки
template<uint32_t address, size_t size, typename AccessMode, 
    typename ...Args>
class Register
{
  private:
 ...

Для того, чтобы установить нужное значение в регистре нам нужно:

  1. Из всего набора значений сформировать одну маску, для обнуления нужных битов в регистрах.
  2. Из всего набора значений сформировать одно значение для установки нужных битов.

Это должны быть constexpr методы, которые сделают все необходимые действия на этапе компиляции:

//Класс для работы с регистром, можно передавать список значений полей для установки и проверки
template<uint32_t address, size_t size, typename AccessMode, 
    typename ...Args>
class Register
{
private:
  //Вспомогательный метод, возвращает маску для 
  //конкретного значения поля на этапе компиляции.
  __forceinline template<typename T>
  static constexpr auto GetIndividualMask()
  {
    Type result = T::Mask << T::Offset ;
    return result ;
  }
  
  //Вспомогательный метод, рассчитывает общую маску 
  //для всего набора значений полей на этапе компиляции.
  static constexpr auto GetMask()
  {
    //распаковываем набор битовых полей через список инициализации
    const auto values = {GetIndividualMask<Args>()...} ;  
    Type result = 0UL;
    for (auto const v: values)
    {
      //для каждого значения поля устанавливаем битовую маску
      result |= v ;  
    }
    return result ;
  }

  //Точно также для значения  
  __forceinline template<typename T>
  static constexpr auto GetIndividualValue()
  {
    Type result = T::Value << T::Offset ;
    return result ;
  }
  
  static constexpr auto GetValue()
  {
    const auto values = {GetIndividualValue<Args>()...};
    Type result = 0UL;
    for (const auto v: values)
    {
      result |= v ;
    }
    return result ;
  }
};

Осталось определить только публичные методы Set() и IsSet():

//Класс для работы с регистром, можно передавать список значений полей для установки и проверки
template<uint32_t address, size_t size, typename AccessMode,  typename ...Args>
class Register
{
public:
  using Type = typename RegisterType<size>::Type;

  template<typename T = AccessMode,
          class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>>
  static void Set()
  {
    Type newRegValue = *reinterpret_cast<Type *>(address) ; 

    //GetMask() вызывается на этапе компиляции, тут будет подставлено значение
    newRegValue &= ~GetMask() ; 

    //GetValue() вызывается на этапе компиляции, тут будет подставлено значение
    newRegValue |= GetValue() ; 

    //Записываем в регистра новое значение
    *reinterpret_cast<Type *>(address) = newRegValue ; 
  }
  
  template<typename T = AccessMode,
          class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>>
  static bool IsSet()
  {
    Type newRegValue = *reinterpret_cast<Type *>(address) ;
    return ((newRegValue & GetMask()) == GetValue()) ;
  }
private:
...

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

int main()
{
  //установили биты, которых нет в регистре TIM1::CR1 
  TIM1::CR1<TIM2::CR1::Enabled, 
           TIM1::CR2::OIS1::OC1OutputIs0>::Set() ;
}

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

Добавляем проверку на принадлежность значения поля данному регистру
template<uint32_t address, size_t size, typename AccessMode, 
        typename FieldValueBaseType,  typename ...Args>
class Register
{
private:
    //Метод будет выведен только если BaseType значения поля является типом FieldValueBaseType, переданным в параметре шаблона.
  __forceinline template<typename T,
          class = typename std::enable_if_t<std::is_same<FieldValueBaseType, 
          typename T::BaseType>::value>>
  static constexpr auto GetIndividualMask()
  {
    Type result = T::Mask << T::Offset ;
    return result ;
  }
  
  static constexpr auto GetMask()
  {
    const auto values = {GetIndividualMask<Args>()...} ;  
    Type result = 0UL;
    for (auto const v: values)
    {
      result |= v ;  
    }
    return result ;
  }

  //Метод будет выеден только если BaseType значения поля является типом FieldValueBaseType, переданным в параметре шаблона.   
  __forceinline template<typename T,
          class = typename std::enable_if_t<std::is_same<FieldValueBaseType, 
       typename T::BaseType>::value>>
  static constexpr auto GetIndividualValue()
  {
    Type result = T::Value << T::Offset ;
    return result ;
  }
  
  static constexpr auto GetValue()
  {
    const auto values = {GetIndividualValue<Args>()...};
    Type result = 0UL;
    for (const auto v: values)
    {
      result |= v ;
    }
    return result ;
  }
};


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

Законченное описание регистра CR1 таймера TIM1, будет выглядеть так:

struct TIM1
{
  struct TIM1CR1Base {} ;

  struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode>
  {
    using CKD = TIM_CR_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode, TIM1CR1Base> ;
    using ARPE = TIM_CR_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode, TIM1CR1Base> ;
    using CMS = TIM_CR_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode, TIM1CR1Base> ;
    using DIR = TIM_CR_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode, TIM1CR1Base> ;
    using OPM = TIM_CR_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode, TIM1CR1Base> ;
    using URS = TIM_CR_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode, TIM1CR1Base> ;
    using UDIS = TIM_CR_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode, TIM1CR1Base> ;
    using CEN = TIM_CR_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode, TIM1CR1Base> ;
  } ;
}

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

Теперь давайте вернемся к первоначальному варианту на Си, где мы наделали кучу ерунды:

Первоначальный вариант
int main(void)
{
  // Включаем тактирование на порту GPIOA
   
  //опечатка, должен быть регистр AHB1ENR 
   RCC->APB1ENR |= RCC_AHB1ENR_GPIOAEN ; 

  //Хотели установить только один бит, но обнулили все биты регистра 
  RCC->AHB1ENR = RCC_AHB1ENR_GPIOAEN; 

  //Неправильно, таймер TIM1 подключен к шине APB2 
  RCC->APB1ENR  |=  RCC_APB1ENR_TIM2EN | RCC_APB2ENR_TIM1EN;
 
  //Видимо кто-то решил, что можно считать состояние порта из этого регистра.      
  auto result = GPIOA->BSRR ; 
  if (result & GPIO_BSRR_BS1)  
  {
     //do something
  }
 
  //Кому-то платят за количество строк кода. Так ведь можно...
  GPIOA->IDR = GPIO_IDR_ID5 ;

И попробуем сделать тоже самое с новым подходом:

int main(void)
{
   // Включаем тактирование на порту GPIOA
   //Ошибка компиляции, у регистра APB1ENR нет поля GPIOAEN
   RCC::APB1ENR::GPIOAEN::Enable::Set() ; 
  
   //Все хорошо, подали тактирование на порт GPIOA
   RCC::AHB1ENR::GPIOAEN::Enable::Set() ; 

   //Ошибка компиляции, RCC::APB2ENR::TIM1EN::Enable не 
   //является полем регистра APB1ENR
   RCC::APB1ENRPack<RCC::APB1ENR::TIM2EN::Enable,
                    RCC::APB2ENR::TIM1EN::Enable>::Set();

   //Ошибка компиляции, регистр BSRR только для записи     
   auto result = GPIOA::BSRR::Get() ; 

   //Ошибка компиляции, значение Reset только для записи
   if (GPIOA::BSRR::BS1::Reset::IsSet())  
   {
      //do something
   }
   
   //Ошибка компиляции, значение поля регистра только для чтения
   GPIOA::IDR::IDR5::On::Set() 
}

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

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

Быстродействие


Для сравнения насколько наш подход оптимален, будем использовать код на Си и С++, который выполняют подачу тактирования на порт А, установку трех портов в режим выхода и установку в этих трех портах 1 на выводах:

Код на Си:
int main()
{  
  uint32_t res = RCC->AHB2ENR;
  res &=~ RCC_AHB1ENR_GPIOAEN_Msk ;
  res |= RCC_AHB1ENR_GPIOAEN ;
  RCC->AHB2ENR = res ; 
  
  res = GPIOA->MODER ;
  res &=~ (GPIO_MODER_MODER5 | 
           GPIO_MODER_MODER4 | 
           GPIO_MODER_MODER1) ;
  res |= (GPIO_MODER_MODER5_0 | 
          GPIO_MODER_MODER4_0 |
          GPIO_MODER_MODER1_0) ;
  GPIOA->MODER = res ;
  
  GPIOA->BSRR = (GPIO_BSRR_BS5 | GPIO_BSRR_BS4 | GPIO_BSRR_BS1) ; 
  
  return 0 ;
}


Код на С++:
int main()
{
  
  RCC::AHB1ENR::GPIOAEN::Enable::Set() ;
  
  GPIOA::MODERPack<
         GPIOA::MODER::MODER5::Output,
         GPIOA::MODER::MODER4::Output,
         GPIOA::MODER::MODER1::Output>::Set() ;
   
  GPIOA::BSRRPack<
         GPIOA::BSRR::BS5::Set,
         GPIOA::BSRR::BS4::Set,
         GPIOA::BSRR::BS1::Set>::Write() ;

  return 0 ;
}


Я использую компилятор IAR. Посмотрим два режима оптимизации: Без оптимизации и на средней оптимизация:

Код на Си и ассемблерное представление без оптимизации:

image

Код на C++ и ассемблерное представление без оптимизации:

image

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

Проверяем на средней оптимизации, код на Си:

image

Как и предполагалось уже всего 13 ассемблерный строк.

И код на С++ на средней оптимизации:

image

Опять же ситуация идентичная: никаких накладных, при очевидном преимуществе в читабельности кода.

Ну что же все задачи решены, возникает заключительный вопрос. Сколько же времени и усилий нужно, чтобы описать все регистры в таком виде?

Как описать все регистры


Мы получили, надежный, удобный и быстрый доступ к регистрам. Остался один вопрос. Как описывать все регистры, их же там под сотню для микроконтроллера. Это же сколько времени надо, чтобы все регистры описать, ведь можно допустить кучу ошибок в такой рутинной работе. Да, в ручную это делать не надо. Вместо этого воспользуемся генератором кода из SVD файла, который, как я указал выше в начале статьи полностью покрывает ту абстракцию регистра, которую я принял.

Я доработал скрипт коллеги, который, на основании этой идеи сделал примерно тоже самое, но немного проще, используя enum, вместо классов для значений полей. Скрипт сделан только для теста и проверки идеи, поэтому не оптимален, но позволяет сгенерировать примерно вот такое.
image
Кому интересно скрипт лежит тут

Итог


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

#include "gpioaregisters.hpp" //for GPIOA
#include "rccregisters.hpp"   //for RCC

int main()
{
  RCC::AHB1ENR::GPIOAEN::Enable::Set() ;
  GPIOA::MODER::MODER15::Output::Set() ;  
  GPIOA::MODERPack<
          GPIOA::MODER::MODER12::Output,
          GPIOA::MODER::MODER14::Analog
  >::Set() ;
}

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

Но, как я уже говорил выше, не все производители заботятся о своих потребителях, поэтому не у всех в файле SVD описаны перечисления, из-за этого для ST микроконтроллеров все перечисления, после генерации выглядят примерно так:

template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> 
struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using Value0 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ;
  using Value1 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ;
  using Value2 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ;
  using Value3 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ;
} ;

В тот момент когда нужно их использовать, можно заглянуть в документацию и поменять слова Value, на что-то более внятное:

template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> 
struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> 
{
  using Input = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ;
  using Output = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ;
  using Alternate = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ;
  using Analog = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ;
} ;

После этого уже все ваши значения полей будут иметь понятное название.

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

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

Собственно все, любые предложения и замечания приветствуются.

Проект под IAR 8.40.1 лежит тут
Сами исходники лежат тут
Код «на Online GDB»

PS: Спасибо putyavka за найденный баг в методе RegisterField::Get()
и Ryppka за найденный баг с assert.

Ссылки и статьи используемые в статье


Typesafe Register Access in C++
One Approach to Using Hardware Registers in C++
SVD Description (*.svd) Format

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


  1. wasserati
    07.09.2019 21:08
    +1

    Спасибо за статью.
    Сам задумывался над чем-то подобным. Только знаний С++ для реализации не хватило.
    Частично решал проблему с помощью макросов вроде:

    #define WRITE_BITFIELD(MOD, N,  REG, FIELD, VAL)   { (MOD ## N)->REG = ((MOD ## N)->REG &= ~(MOD ## _ ## REG ## _ ## FIELD)) | \ (uint32_t)(VAL) << (MOD ## _ ## REG ## _ ## FIELD ## _ ## Pos); }

    Что выливалось в код:
    WRITE_BITFIELD(I2C, 1, TIMINGR, PRESC,  0x0B)

    Не оптимальное решение, но от ошибок вроде записи не в тот регистр спасало.
    Теперь можно и ряд других ошибок отлавливать.
    Можно еще доработать подход и использовать его для работы с регистрами различных ИС подключаемых посредством I2C или SPI, или пр. Писать придется в переменную, а не по адресу. А уже потом эту переменную передавать.


  1. yakk
    07.09.2019 21:09

    18 линий ассемблера
    /facepalm, с каким пор строки стали называться линиями?


    1. lamerok Автор
      07.09.2019 21:10

      Подправил.


  1. Goron_Dekar
    08.09.2019 22:40
    -1

    Эта статья великолепна! У меня наконец появилась ссылка, которой можно кидать в тех, кто говорит, что плюсы это оверхед.


    1. mapron
      08.09.2019 08:54

      Данная статья не годится для кидания, т.к. почти все можно и макросами сделать (см первый коммент).
      Мне больше понравилась вот эта статья трехлетней давности:
      habr.com/ru/post/357910


      1. lamerok Автор
        08.09.2019 09:14

        Кидаться точно не стоит. Статья написана для того, чтобы показать преимущество строгой типизации С++ на Си. И я приверженец того, что макросы это зло: habr.com/ru/post/246971, так как по опыту знаю, что они источник потенциальных ошибок.


        1. mapron
          08.09.2019 10:06

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


          1. yakk
            08.09.2019 10:48

            ну, на самом деле и обычный c достаточен в основном, битовые поля есть… другое дело что с плюсами можно сделать чуть удобнее, но насчет безопаснее — не уверен…


  1. Goshik
    07.09.2019 23:30

    Давно мечтал о такой штуке с zero-cost abstraction. Такая статическая проверка корректности намного лучше, чем те костыльные HAL библиотеки от ST, с которыми ошибки нужно ловить в рантайме. Они еще, к тому же, сильно раздувают бинарник. Помнится HAL для таймера вообще не влезал в чип со средним размером флеша, если не включить оптимизацию.


    1. lamerok Автор
      08.09.2019 09:51

      Да с HAL точно перебор по коду. Кроме того, там много ошибок, особенно с USB. Правда надо отдать должное, все исправляется довольно оперативно.


  1. Goshik
    08.09.2019 04:24

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

    Не думали-ли вы над реализаций различных представлений для одних и тех же полей? Например, в одних случаях порт ввода/вывода полезно представить как набор отдельных пинов. В других, на него может понадобиться выводить 8-битное целое число. Насколько я понимаю, сейчас доступен только второй вариант, в то время как со стандартным подходом можно реализовать оба варианта.


    1. lamerok Автор
      08.09.2019 12:01

      Ну это не совсем библиотека, это только подход, я его использую для обучения студентов. По поводу показывает ли среда поля. Это зависит от среды. Clion показывает все, т. е., там вообще думать не надо., но сам по себе он сыроват для того, чтобы полноценно использовать для разработки встроенного ПО. IAR, тоже показывает, но со странностями :)., иногда выдаёт какую-то чушь, но чаще работает нормально.
      Про остальное пока не думал, задача была, по большому счету сделать так, чтобы студенты ошибок меньше делали. А то на это уходило много времени у меня, ещё ведь нормальная работа есть, помимо обучения. Хотелось минимизировать трату времени на студентов, которые документацию читают сквозь пальцы.


  1. conf1ict
    08.09.2019 07:17

    Работал с подобной библиотечкой для STM32, написанной шаблонным мастером. Перескажу впечатления: куча шаблонов быстро приводят код в нечитаемое состояние, низкая скорость компиляции, невозмножно дебажить — при попытке поставить бряк в шаблонную функцию ругается программатор, большая вложенность неймспейсов отбивает все желание набивать раз за разом одно и то же. Главное, что оно нисколько не помогает: код как был кучей записей в регистры, так им и остался. По мне лучше делать абстракции на уровне периферии целиком с дружелюбными названиями методов. А с регистрами советую не заморачиваться: чем их меньше торчит в коде, тем лучше.


    1. lamerok Автор
      08.09.2019 09:52

      Ну идея тут как раз и заключалась в том, чтобы шаблонами не злоупотреблять, для использования регистров — шаблонов нет уже:

      #include "gpioaregisters.hpp" //for GPIOA
      #include "rccregisters.hpp"   //for RCC
      
      int main()
      {
        RCC::AHB1ENR::GPIOAEN::Enable::Set() ;
        GPIOA::MODER::MODER15::Output::Set() ;  
      }

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


  1. mctMaks
    08.09.2019 07:17

    моё почтенье после прочтения, было интересно и познавательно. но позволю себе пару вставить пару комментариев:

    Без комментариев тут не обойтись. Хотя код всего-то устанавливает скорость работы порта GPIOA.0 в значение 40 Мгц

    тут не совсем так, скорее даже совсем не так. Порт как работал со «скоростью» шины AHB так и будет работать. Регистр OSPEEDR определяет скорость нарастания фронта на выводе настроенном как выход. Кстати, в более новых поделиях этой фирмы, они заменили конкретные цифры на абстракции вида VeryLow\Low\Medium\High или их вариации.

    Но, как я уже говорил выше, не все производители заботятся о своих потребителях, поэтому не у всех в файле SVD описаны перечисления, из-за этого для ST микроконтроллеров все перечисления, после генерации выглядят примерно так

    я наверно Вас удивлю сейчас, но для некоторых МК st не то что не описали перечисления, они некоторые периферийные модули забыли описать. Так что полностью автоматизировать вряд ли получится. Для примера реальная история с stm32l4r4zi и попыткой найти DMAMUX в svd файле:

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


    1. lamerok Автор
      08.09.2019 09:44

      Спасибо за комментарий, про порты поправил, про SVD описание, согласен, ST как то очень легкомысленно к этому относится. Я предполагал, что они генерируют описания из каких-то инженерных документов, которые используются для производства микроконтроллера автоматически.
      Кстати, я не смог и просто Си заголовочник найти для STM32L4.


      1. mctMaks
        08.09.2019 14:52

        и снова ошибка. не скорость переключения, а именно время нарастания фронта (то есть его крутизну). За примером в даташит, а именно поискать таблицу "I/O AC characteristics" и посмотреть там строки «Output rise and fall time».
        Из-за своего названия этот регистр много путаницы вносит в понимание работы такого просто модуля, как GPIO.

        как правило заголовочники можно вытащить из IDE, которая пользуется отдельными паками для вендоров (Keil/Segger Embedded Studio например), ну или выкачивая монструозные библиотеки HAL\LL на все семейство.


        1. lamerok Автор
          08.09.2019 15:35

          Да, но там же в спецификации есть и параметр Максимальная частота. Т. Е. установка битов влияет на оба параметры, и они взаимосвязаны. Можно ли сказать, что просто максимальную частоту?


          1. mctMaks
            08.09.2019 16:51

            в принципе наверно можно и так сказать.
            но опять же, смотрите. Вот выставили мы например значение 00 в регистр. Это же не означает, что МК не сможет и не позволит вывести через данный пин сигнал в 40МГц. Меандр будет на выходе, но очень уж близкий к синусу. Если Вам нужно работать с цифровым интерфейсом, то это проблема. Вот и получается, что это не сколько частота работы (максимальная\разрешенная), сколько признак того насколько меандр выведенный с данного пина будет меандром.

            я надеюсь Вы понимаете что я не пытаюсь придираться)


            1. lamerok Автор
              09.09.2019 10:58

              Исправил по вашему комментарию. Конечно, понимаю, я за любое конструктивное обсуждение, спасибо, за пояснение.


  1. AVI-crak
    08.09.2019 12:51

    Красивый метод, но не универсальный. Дело в том что кроме установки отдельных битов регистра — есть ещё одна зависимость, которую даже не сразу видно. Это внутренние триггеры запуска событий.
    Например для таймера это поле называется CEN, и просто запускает таймер в работу. А вот для qspi есть сразу четыре внутренних триггера. Которые явно зависят от состояния полей. Установка QUADSPI_CR_EN — лишь разрешает работу этих триггеров, всё остальное требует строгой последовательности работы с регистрами и данными.
    Причём для qspi, также как и для sd интерфейса — ошибки в алгоритме в работе с регистрами приводят к потери информации.

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


  1. Alex_ME
    08.09.2019 13:32

    Пытался сделать что-то подобное, правда, дальше экспериментов дело не дошло.
    Вместо параметра шаблона, определяющего режим доступа и enable_if банально наследовал регистры от от чего-то вроде RegR с определенным методом get и/или RegW с методом set.


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


  1. rogoz
    08.09.2019 14:11

    template<uint32_t address, size_t size, typename AccessMode>
    struct RegisterBase
    наверно лучше
    uintptr_t address


    1. lamerok Автор
      08.09.2019 18:16

      Вы имеете ввиду передавать в шаблон uintptr_t — это же указатель, т.е. к нему нужно будет привести, а чтобы привести опять таки будет использоваться reinterpret_cast, а его использовать в шаблонах нельзя? или приводить уже внутри к типу uintptr_t.
      Я что-то не уловил. Можете уточнить. Кроме того, насколько я помню, этот тип поддерживается опционально, т.е. компилятор может его и не поддерживать.


      1. rogoz
        08.09.2019 20:00

        Ну во первых этот тип специально сделан для представления указателя в виде числа. Имхо, хороший тон его использовать, даже если дальше reinterpret_cast будет такой-же.
        Во вторых, будут проблемы, если ваш код когда-нибудь попадёт, например, на машину с 64-битной адресацией.
        Насчёт поддержки в реальности на всех платформах не знаю, но если sizeof(uint32_t) == sizeof(void*), то не добавить sizeof(uintptr_t) == sizeof(uint32_t) как-то тупо.


  1. 0xd34df00d
    08.09.2019 18:56
    +1

    Годнота.


    Вообще я давно уже читаю ваши статьи и думаю, что вам может быть пора переходить на вещества потяжелее.


    1. lamerok Автор
      09.09.2019 14:39

      :) Спасибо, за наводку, занятно. Если будет время, попробую поразбираться повнимательнее, взглянул мельком, выглядит многообещающе.


  1. IgorPie
    09.09.2019 09:54

    Затея неплохая, но централизации по шаблонизации не предвидится, а значит будет кто во что горазд.
    Ну и выглядит это как «защита детей», когда на все что только можно клеятся защитные шалабушки, чтобы не ударился. В результате дети становятся непуганные и несамостоятельные. А если бы пару раз вдарившись, стали бы думать прежде чем делать.

    Кроме того, производители софта не торопятся делиться им бесплатно. Откуда у вас KEIL и IAR на cm4? В суровой реальности, если софт не краденный, то это gcc + эклипс или иной редактор.


    1. Sun-ami
      09.09.2019 14:10
      +1

      Откуда у вас KEIL и IAR на cm4? В суровой реальности, если софт не краденный, то это gcc + эклипс или иной редактор.
      Всё не так однозначно, у IAR есть триальная лицензия с ограничением по размеру прошивки, пригодная для учебных целей.


    1. lamerok Автор
      09.09.2019 14:23

      IAR, конкретно у меня, лицензионный, а для студентов специальная версия с ограничением по размеру коду (30 кБ) — для учебных целей подходит.


      1. IgorPie
        09.09.2019 17:01

        Лицензионный софт — это хорошо, но 0 кило — мало же. А студенты будут уже подсажены на эту обёртку, получается.


    1. 0xd34df00d
      09.09.2019 16:33

      Ну и выглядит это как «защита детей», когда на все что только можно клеятся защитные шалабушки, чтобы не ударился. В результате дети становятся непуганные и несамостоятельные. А если бы пару раз вдарившись, стали бы думать прежде чем делать.

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


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


      1. IgorPie
        09.09.2019 17:00

        У меня тоже память как у золотой рыбки. Поэтому не жалею комментариев.


        1. 0xd34df00d
          09.09.2019 17:14

          Только компилятор не проверяет комментарии.


  1. Ryppka
    10.09.2019 21:48

    assert(value < ((1 << size) - 1)) ;

    Не совсем понял, эта проверка для поля шириной, например, 3 бита не разрешит ввести значение 7. Так и задумано? Или я что-то недогнал по позднему времени?


    1. lamerok Автор
      11.09.2019 12:17

      Ага, ошибка, надо убрать -1. Copy past. Спасибо! Peer review в действии.