10++ способов работать с аппаратными регистрами на С++ (на примере IAR и Cortex M)

Choosing the safest path
Рис. И. Кийко

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

Помните наверное бородатый анекдот, а может быть и правдивую историю про то, как студента спрашивали о способе измерить высоту здания с помощью барометра. Студент привел, по-моему около 20 или 30 способов, при этом не назвав прямого (через разницу давления), которого ожидал преподаватель.

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

Способ 1. Очевидный и, очевидно, не самый лучший


Самый распространенный способ, который также применяется в С++, является использование описания структур регистров из заголовочного файла от производителя. Для демонстрации я возьму два регистра порта А (ODR — регистр выходных данных и IDR — регистра входных данных) микроконтроллера STM32F411, чтобы можно было выполнить «ембедерский» «Hello world» — моргнуть светодиодом.

int main() {
  GPIOA->ODR ^= (1 << 5) ;
  GPIOA->IDR ^= (1 << 5) ; //ГЛУПОСТЬ, но я же не знал
}


Давайте посмотрим, что тут происходит, и как эта конструкция работает. В заголовочнике для микропроцессора есть структура GPIO_TypeDef и определение указателя на эту структуру GPIOA. Выглядит это следующим образом:

typedef struct
{
  __IO uint32_t MODER;   //port mode register,  Address offset: 0x00      
  __IO uint32_t OTYPER;  //port output type register,  Address offset: 0x04
  __IO uint32_t OSPEEDR; //port output speed register,  Address offset: 0x08
  __IO uint32_t PUPDR;   //port pull-up/pull-down register, Address offset: 0x0C
  __IO uint32_t IDR;     //port input data register,  Address offset: 0x10 
  __IO uint32_t ODR;     //port output data register, Address offset: 0x14
  __IO uint32_t BSRR;    //port bit set/reset register, Address offset: 0x18
  __IO uint32_t LCKR;    //port configuration lock register, Address offset: 0x1C
  __IO uint32_t AFR[2];  //alternate function registers, Address offset: 0x20-0x24
} GPIO_TypeDef;

#define PERIPH_BASE     0x40000000U //Peripheral base address in the alias region  
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000U)
#define GPIOA_BASE          (AHB1PERIPH_BASE + 0x0000U)

#define GPIOA             ((GPIO_TypeDef *) GPIOA_BASE)


Если выразиться простыми человеческими словам, то вся структура типа GPIO_TypeDef «ложится» по адресу GPIOA_BASE, а при обращении к конкретному полю структуры, вы по сути обращается к адресу этой структуры + смещение до элемента этой структуры. Если убрать #define GPIOA, то код выглядел бы так:

((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;
((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ; //ГЛУПОСТЬ


Применительно к языку программирования С++ здесь происходит преобразование целочисленного адреса к типу указатель на структуру GPIO_TypeDef. Но в С++ при использовании Си преобразования компилятор пытается выполнить преобразование в следующей последовательности:

  • const_cast
  • static_cast
  • static_cast следующей за const_cast,
  • reinterpret_cast
  • reinterpret_cast следующий за const_cast


т.е. если компилятор не смог преобразовать тип используя const_cast, он пытается применить static_cast и так далее. В итоге вызов:

((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;


есть ни что иное как:

reinterpret_cast (GPIOA_BASE)->ODR ^= (1 << 5) ;


На самом деле для С++ приложений правильно было бы «натянуть» структуру на адрес вот так:

GPIO_TypeDef * GPIOA{reinterpret_cast(GPIOA_BASE)} ;


В любом случае из-за преобразования типов существует большой минус этого подхода для С++. Заключается он в том, что reinterpret_cast нельзя использовать ни в constexpr конструкторах и функциях, ни в параметрах шаблона, а это существенно сужает использование возможностей С++ для микроконтроллеров.
Поясню это на примерах. Вполне возможно сделать так:

 struct Test {
  const int a;
  const int b;
} ;

template
constexpr const int Geta() {
  return mystruct->a;
}

Test test{1,2};
int main() {
  Geta<&test>() ;
}


Но вот так уже сделать нельзя:

 
template
constexpr volatile uint32_t GetIdr() {
  return mystruct->IDR;
}
int main() {
//GPIOA это  reinterpret_cast (GPIOA_BASE) 
//использует преобразование типов, и в параметры шаблона его передавать нельзя
  GetIdr() ; //Ошибка
}

// И вот так тоже сделать нельзя:
struct Port {
  constexpr Port(GPIO_TypeDef * ptr): port(*ptr) {} 
  GPIO_TypeDef & port ;
}
//Так как GPIOA использует reinterpret_cast, то конструктор 
//перестает быть constexpr и невозможно выполнить статическую инициализацию
constexpr Port portA{GPIOA}; // тут будет ошибка


Таким образом прямое использование такого подхода накладывает существенные ограничения на использование С++. Мы не сможем расположить объект который хочет использовать указатель на GPIOA в ROM используя средства языка, и не сможем использовать преимущества метапрограммирования для такого объекта.
Кроме того, вообще такой способ не safety (как говорят наши западные партнеры). Ведь вполне возможно сделать какую-то ГЛУПОСТЬ
В связи с вышесказанным резюмируем:

Плюсы


  • Используется заголовочник от производителя (он проверен, в нем нет ошибок)
  • Нет дополнительных телодвижений и затрат, берешь и используешь
  • Простота использования
  • Все знают и понимают этот способ
  • Никаких накладных

Минусы


  • Ограниченное использование метапрограммирования
  • Невозможность использовать в constexpr конструкторах
  • При использовании в классах обертках, дополнительных расход ОЗУ, на указатель на объект этой структуры
  • Можно сделать ГЛУПОСТЬ

Теперь посмотрим на способ №2

Способ 2. Брутальный


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

*reinterpret_cast(GpioaOdrAddr) ^= (1 <<5) ;
*reinterpret_cast(GpioaIdrAddr) ^= (1 <<5) ; //ГЛУПОСТЬ


В любом месте программы, всегда можно вызвать преобразование к volatile uint32_t адресу регистра и установить там хоть что.
Плюсов тут особо нет, а к тем минусам, что есть добавится еще неудобство использования и необходимость самому прописывать адрес каждого регистра во отдельном файле. Поэтому переходим в способу №3.

Способ 3. Очевидный и очевидно правильнее


Если доступ к регистрам происходит через поле структуры, то вместо указателя на объект структуры можно использовать целочисленный адрес структуры. Адрес структур есть в заголовочном файле от производителя (например, GPIOA_BASE для GPIOA), поэтому его не надо помнить, а применять можно и в шаблонах и в constexpr выражениях, а затем уже «накладывать» структуру на этот адрес.

template
  struct Pin {   
      using Registers = GPIO_TypeDef ;
      __forceinline static void Toggle() {
        // располагаем структуру по адресу addr
        Registers *GpioPort{reinterpret_cast(addr)}; 
        GpioPort->ODR ^= pinNum ;
      }
  };
int main() {
  using Led1 =  Pin ;
  Led1::Toggle() ;
}


Особых минусов, с моей точки зрения нет. В принципе рабочий вариант. Но все равно, давайте разберем другие способы.

Способ 4. Экзотерическая обертка


Для ценителей понятного кода, можно сделать обертку над регистром, чтобы обращаться к ним было удобно и выглядело «красиво», сделать конструктор, переопределить операторы:

class Register  {
    public:
      explicit Register(uint32_t addr) : ptr{ reinterpret_cast(addr) } {
      }

      __forceinline inline Register& operator^=(const uint32_t right)  {
        *ptr ^= right;
        return *this;
      }

    private:
      volatile uint32_t *ptr; //указатель хранящий адрес регистра
  };

int  main() {
    Register Odr{GpioaOdrAddr};
    Odr ^= (1 << 5);
    Register Idr{GpioaIdrAddr};
    Idr ^= (1 << 5); //ГЛУПОСТЬ
}


Как видно, снова придется либо помнить целочисленные адреса всех регистров, либо где-то их задавать, а еще придется хранить указатель, на адрес регистра. Но что опять не очень, снова в конструкторе происходит reinterpret_cast
Одни минусы, а к тем, что в первом и втором варианте добавилась еще необходимость на каждый используемый регистр хранить указатель в 4 байта в ОЗУ. В общем не вариант. Смотрим следующий.

Способ 4,5. Экзотерическая обертка с шаблоном


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

template
  class Register  {
    public:
      Register() : ptr{reinterpret_cast(addr)}  {
      }

      __forceinline inline Register &operator^=(const uint32_t right)  {
        *ptr ^= right;
        return *this;
      }

    private:
      volatile std::uint32_t *ptr;
  };

int main() {
    using GpioaOdr = Register;
    GpioaOdr Odr;
    Odr ^= (1 << 5);
    using GpioaIdr = Register;
    GpioaIdr Idr;
    Idr ^= (1 << 5); //ГЛУПОСТЬ
}


А так, те же грабли, вид сбоку.

Способ 5. Разумный


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

template
  class Register  {
    public:
      __forceinline  Register &operator^=(const uint32_t right)   {
        *reinterpret_cast(addr) ^= right;
        return *this;
      }
  };
   using GpioaOdr = Register;
    GpioaOdr Odr;
    Odr ^= (1 << 5);
    using GpioaIdr = Register;
    GpioaIdr Idr;
    Idr ^= (1 << 5); //ГЛУПОСТЬ


Можно остановиться здесь и немного порассуждать. Это способ сразу решает 2 проблемы, которые до этого наследовались от первого метода. Во первых, теперь я могу использовать указатель на объект Register в шаблоне, а во вторых я его могу передавать в constexrp конструктор.

template
constexpr uint32_t Get() {
  return register::Get();
}
Register  GpioAOdr;
int main() {
  Geta<&GpioaOdr>() ; //Все Ок
}
//и так могу
struct Port {
  constexpr Port(Register& ref): port(ref) {} 
  Register & register ;
}
constexpr Port portA{GpioaOdr}; 


Конечно, нужно снова, либо обладать эйдетической памятью на регистры, либо определить руками все адреса регистров где-то…

Плюсы


  • Простота использования
  • Возможность использования метапрограммирования
  • Возможность использовать в constexpr конструкторах

Минусы


  • Не используется проверенный заголовочный файл от производителя
  • Надо самому задавать все адреса регистров
  • Нужно создавать объект класс Register
  • Можно сделать ГЛУПОСТЬ


Отлично, но минусов все еще много…

Способ 6. Разумнее разумного


В предыдущем методе, чтобы обратиться к регистру необходимо было создать объект этого регистра, это ненужные траты ОЗУ и ПЗУ, поэтому делаем обертку со статическими методами.

template
  class Register  {
    public:
      __forceinline  inline static void Xor(const uint32_t mask)
      {
        *reinterpret_cast(addr) ^= mask;
      }
  };
int main() {
    using namespace Case6 ;
    using Odr = Register;
    Odr::Xor(1 << 5);
    using Idr = Register;
    Idr::Xor(1 << 5); //ГЛУПОСТЬ
}


Добавляется один плюс

  • Никаких накладных. Быстрый компактный код, такой же как и в варианте 1 (При использовании в классах обертках, нет дополнительных расходов ОЗУ, так как объект не создается, а используются статические методы без создания объектов)

Идем дальше…

Способ 7. Убираем ГЛУПОСТЬ


Очевидно, я сделал ГЛУПОСТЬ в коде и записал что-то в регистр, который на самом деле для записи не предназначен. Ничего страшного, конечно, но ГЛУПОСТИ надо запрещать. Давайте запретим делать ГЛУПОСТИ. Для этого введем вспомогательные структуры:

  struct WriteReg {};
  struct ReadReg {};
  struct ReadWriteReg: public WriteReg, public ReadReg {};


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

template
  class Register 
  {
    public:
     //Если в параметр шаблона будет передавать тип WriteReg, то метод будет
    // инстанциирован, если нет, то такого метода существовать не будет 
      __forceinline template ::value>>
      Register &operator^=(const uint32_t right)
      {
        *reinterpret_cast(addr) ^= right;
        return *this;
      }
  };


Теперь попробуем откомпилировать наш тест и увидим, что тест не компилируется, потому что оператора ^= для регистра Idr не существует:

   int main()  {
    using GpioaOdr  = Register ;
    GpioaOdr Odr ;
    Odr ^= (1 << 5) ;
    using GpioaIdr  = Register ;
    GpioaIdr Idr ;
    Idr ^= (1 << 5) ; //ошибка, регистра Idr только для чтения
  }


Итак, теперь плюсов становится больше…

Плюсы


  • Простота использования
  • Возможность использования метапрограммирования
  • Возможность использовать в constexpr конструкторах
  • Быстрый компактный код, такой же как и в варианте 1
  • При использовании в классах обертках, нет дополнительных расходов ОЗУ, так как объект не создается, а используются статические методы без создания объектов
  • Нельзя сделать ГЛУПОСТЬ

Минусы


  • Не используется проверенный заголовочный файл от производителя
  • Надо самому задавать все адреса регистров
  • Нужно создавать объект класс Register


Что же давайте уберем возможность создавать класс, чтобы еще сэкономить

Способ 8. Без ГЛУПОСТИ и без объекта класса


Сразу код:

  struct WriteReg {};
  struct ReadReg {};
  struct ReadWriteReg: public WriteReg, public ReadReg {};

  template
  class Register  {
      public:
      __forceinline template ::value>>
        inline static void Xor(const uint32_t mask)  {
          *reinterpret_cast(addr) ^=  mask;
        }
    };

  int main {
    using GpioaOdr  = Register ;
    GpioaOdr::Xor(1 << 5) ;
    using GpioaIdr  = Register ;
    GpioaOdr::Idr(1 << 5) ; //ошибка, регистра Idr только для чтения
  }


Добавляем еще один плюс, объект не создаем. Но идем дальше, у нас еще остались минусы

Способ 9. Способ 8 с объединением в структуру


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

namespace Case9
{
  struct WriteReg {};
  struct ReadReg {};
  struct ReadWriteReg: public WriteReg, public ReadReg {};

  template
  class Register
    {
      public:
      __forceinline template ::value>>
        inline static void Xor(const uint32_t mask)
        {
          *reinterpret_cast(addr) ^=  mask;
        }
    };

  template
  struct Gpio  
  {
    using Moder = Register; //надо знать сдвиг регистра в структуре
    using Otyper = Register ;
    using Ospeedr = Register ;
    using Pupdr = Register ;
    using Idr = Register ;
    using Odr = Register ;
  };

int main() {
    using Gpioa = Gpio ;
    Gpioa::Odr::Xor(1 << 5) ;
    Gpioa::Idr::Xor((1 << 5) ); //ошибка,  регистр Idr только для чтения
  }


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

Способ 10. Обертка над регистром через указатель на член структуры


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

Чтобы узнать смещение регистра относительно начала структуры, можно использовать указатель на член структуры: volatile uint32_t T::*member, он нам вернет смещение члена структуры относительно её начала в байтах. например есть у нас структура
GPIO_TypeDef, то &GPIO_TypeDef::ODR будет равно 0×14. Обыграем эту возможность.

template
class RegisterStructWrapper {
public:
  __forceinline  template
   inline static void Xor(P T::*member, int mask) {
    reinterpret_cast(addr)->*member ^= mask ;
  }  
} ;

using GpioaWarpper = RegisterStructWrapper ;
int main() {
   GpioaWarpper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ;
return 0 ;
}


Плюсы


  • Простота использования
  • Возможность использования метапрограммирования
  • Возможность использовать в constexpr конструкторах
  • Быстрый компактный код, такой же как и в варианте 1
  • При использовании в классах обертках, нет дополнительных расходов ОЗУ, так как объект не создается, а используются статические методы без создания объектов
  • Нельзя сделать ГЛУПОСТЬ
  • Используется проверенный заголовочный файл от производителя
  • Не нужно самому задавать все адреса регистров
  • Не нужно создавать объект класс Register

Минусы


  • Да особо нет, но можно порассуждать на тему понятности кода


Способ 10.5. Объединяем метод 9 и 10


А теперь вычислим адреса регистров из способа 9, с помощью компилятора:

struct WriteReg {};
  struct ReadReg {};
  struct ReadWriteReg: public WriteReg, public ReadReg {};

  template
  class Register {
    public:
      __forceinline template ::value>>
      inline static void Xor(const uint32_t mask)
      {
        reinterpret_cast(addr)->*member ^= mask ;
      }
  };

  template
  struct Gpio
  {
    using Moder = Register;
    using Otyper = Register;
    using Ospeedr = Register;
    using Pupdr = Register;
    using Idr = Register;
    using Odr = Register;
  } ;


Работать с регистрами можно более экзотерично:

using namespace Case11 ;
    using Gpioa = Gpio ;
    Gpioa::Odr::Xor(1 << 5) ;
    //Gpioa::Idr::Xor((1 << 5) ); //ошибка,  регистр Idr только для чтения


Очевидно, что тут придется все структуры переписать снова. Это можно сделать автоматически, каким-нибудь скриптом на Phyton, на входе что-то типа stm32f411xe.h на выходе ваш файл со структурами для использования в С++.
В любом случае, есть несколько различных способов, которые могут подойти в конкретном проекте.

Бонус. Вводим расширение языка и парсим код с помощью Phyton


Проблема работы с регистрами на С++ существует уже давненько. Люди решают её по разному. Конечно было бы замечательно, если бы язык поддерживал что-то типа переименования классов во время компиляции. Ну скажем, а что если было бы так:

template
class Gpio[Portname] {
   __forceinline  inline static void Xor(const uint32_t mask)  {
        GPIO[PortName]->ODR ^=  mask ;
      }
}; 

int main() {
  using GpioA = Gpio<"A"> ;
  GpioA::Xor(5) ;
}


Но к сожалению такого язык не поддерживает. Поэтому решение которое используют люди, это парсинг кода с помощью Python. Т.е. вводится некоторое расширение языка. Код, с использованием этого расширения, подается на Python парсер, который переводит его в С++ код. Такой код выглядит приблизительно так: (пример взят из modm библиотеки вот тут полные исходники):

%% set port = gpio["port"] | upper
%% set reg  = "GPIO" ~ port
%% set pin  = gpio["pin"]
class Gpio{{ port ~ pin }} : public Gpio 
{
    __forceinline  inline static void Xor()  {
        GPIO{{port}}->ODR ^=  1 << {{pin}} ;
      }
}

//С помощью скрипта он преобразуется в следующий код
class GpioС5 : public Gpio 
{
    __forceinline  inline static void Xor()  {
        GPIOС->ODR ^=  1 << 5 ;
      }
}

//А использовать его можно так
using Led = GpioС5;

Led::Xor();


На этом все… мое воображение исчерпалось. Если у вас еще есть идеи, велком. Пример со всеми способами лежит тут

Ссылки


Typesafe Register Access in C++
Making things do stuff -Accessing hardware from C++
Making things do stuff — Part 3
Making things do stuff- Structure overlay

© Habrahabr.ru