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

Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)

Время на прочтение 24 мин
Количество просмотров 16K
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
Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+22
Комментарии 40
Комментарии Комментарии 40

Публикации

Истории

Работа

QT разработчик
15 вакансий
Программист C++
128 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн