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<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ;

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

    GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ;

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

     struct Test {
      const int a;
      const int b;
    } ;
    
    template<Test* mystruct>
    constexpr const int Geta() {
      return mystruct->a;
    }
    
    Test test{1,2};
    int main() {
      Geta<&test>() ;
    }
    

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

     
    template<GPIO_TypeDef * mystruct>
    constexpr volatile uint32_t GetIdr() {
      return mystruct->IDR;
    }
    int main() {
    //GPIOA это  reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE) 
    //использует преобразование типов, и в параметры шаблона его передавать нельзя
      GetIdr<GPIOA>() ; //Ошибка
    }
    
    // И вот так тоже сделать нельзя:
    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<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ;
    *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ; //ГЛУПОСТЬ

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

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


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

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

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

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


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

    class Register  {
        public:
          explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(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<uint32_t addr>
      class Register  {
        public:
          Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)}  {
          }
    
          __forceinline inline Register &operator^=(const uint32_t right)  {
            *ptr ^= right;
            return *this;
          }
    
        private:
          volatile std::uint32_t *ptr;
      };
    
    int main() {
        using GpioaOdr = Register<GpioaOdrAddr>;
        GpioaOdr Odr;
        Odr ^= (1 << 5);
        using GpioaIdr = Register<GpioaIdrAddr>;
        GpioaIdr Idr;
        Idr ^= (1 << 5); //ГЛУПОСТЬ
    }
    

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

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


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

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

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

    template<Register * register>
    void Xor(uint32_t mask) {
      *register ^= mask ;
    }
    Register<GpioaOdrAddr>  GpioaOdr;
    int main() {
      Xor<&GpioaOdr>(1 << 5) ; //Все Ок
    }
    //и так могу
    struct Port {
      constexpr Port(Register& ref): register(ref) {} 
      Register & register ;
    }
    constexpr Port portA{GpioaOdr}; 
    

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

    Плюсы


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

    Минусы


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

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

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


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

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

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

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


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

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

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

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

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

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

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

    Плюсы


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

    Минусы


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

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

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


    Сразу код:

      struct WriteReg {};
      struct ReadReg {};
      struct ReadWriteReg: public WriteReg, public ReadReg {};
    
      template<uint32_t addr, typename T>
      class Register  {
          public:
          __forceinline template <typename T1 = T,
                class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>>
            inline static void Xor(const uint32_t mask)  {
              *reinterpret_cast<volatile int*>(addr) ^=  mask;
            }
        };
    
      int main {
        using GpioaOdr  = Register<GpioaOdrAddr, WriteReg> ;
        GpioaOdr::Xor(1 << 5) ;
        using GpioaIdr  = Register<GpioaIdrAddr, ReadReg> ;
        GpioaIdr::Xor(1 << 5) ; //ошибка, регистр Idr только для чтения
      }
    

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

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


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

    namespace Case9
    {
      struct WriteReg {};
      struct ReadReg {};
      struct ReadWriteReg: public WriteReg, public ReadReg {};
    
      template<uint32_t addr, typename T>
      class Register
        {
          public:
          __forceinline template <typename T1 = T,
                class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>>
            inline static void Xor(const uint32_t mask)
            {
              *reinterpret_cast<volatile int*>(addr) ^=  mask;
            }
        };
    
      template<uint32_t addr>
      struct Gpio  
      {
        using Moder = Register<addr, ReadWriteReg>; //надо знать сдвиг регистра в структуре
        using Otyper = Register<addr + OtyperShift, ReadWriteReg> ;
        using Ospeedr = Register<addr + OspeedrShift,ReadWriteReg> ;
        using Pupdr = Register<addr + PupdrShift,ReadWriteReg> ;
        using Idr = Register<addr + IdrShift, ReadReg> ;
        using Odr = Register<addr + OdrShift, WriteReg> ;
      };
    
    int main() {
        using Gpioa = Gpio<GPIOA_BASE> ;
        Gpioa::Odr::Xor(1 << 5) ;
        Gpioa::Idr::Xor((1 << 5) ); //ошибка,  регистр Idr только для чтения
      }
    

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

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


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

    template<uint32_t addr, typename T>
    class RegisterStructWrapper {
    public:
      __forceinline  template<typename P>
       inline static void Xor(P T::*member, int mask) {
        reinterpret_cast<T*>(addr)->*member ^= mask ; //Обращаемся к члену структуры, который передали в параметре шаблона. 
      }  
    } ;
    
    using GpioaWrapper = RegisterStructWrapper<GPIOA_BASE, GPIO_TypeDef> ;
    int main() {
      GpioaWrapper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ;
      GpioaWrapper::Xor(&GPIO_TypeDef::IDR, (1 << 5)) ;  //ГЛУПОСТЬ
      return 0 ;
    }
    

    Плюсы


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

    Минусы


    • Можно сделать ГЛУПОСТЬ и еще порассуждать на тему понятности кода

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


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

    struct WriteReg {};
      struct ReadReg {};
      struct ReadWriteReg: public WriteReg, public ReadReg {};
    
      template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType>
      class Register {
        public:
          __forceinline template <typename T1 = RegType,
            class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>>
          inline static void Xor(const uint32_t mask)
          {
            reinterpret_cast<T*>(addr)->*member ^= mask ;
          }
      };
    
      template<uint32_t addr, typename T>
      struct Gpio
      {
        using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>;
        using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>;
        using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>;
        using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>;
        using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>;
        using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>;
      } ;

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

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

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

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


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

    template<classname = [PortName]>
    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();
    


    Обновление: Бонус. SVD файлы и парсер на Phyton


    Забыл добавить еще один вариант. ARM выпускает файл описания регистров для каждого производителя SVD. Из которых потом можно сгенерировать С++ файл с описанием регистров. Paul Osborne собрал все эти файлы на GitHub. А также, он написал скрипт на Python для их парсинга.

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

    Ссылки


    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
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 25

      +2
      Студент привел, по-моему около 20 или 30 способов
      приписывают физику по имени Нильс Бор
        0

        Эм. "Экзотерично" это как? Экзотично? Эзотерично? Шестнадцатирично? :-)
        А вообще конечно странно, что до сих пор не написали каноничный код для работы с регистрами мк. На хабре статей 5 уже видел...

          0

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

          +3
          Типичный случай забивания гвоздей микроскопом.
          Нафига вам работать с регистрами из C++ напрямую?
          Вы пишите BSP так что потом вас долго вспоминали тихим добрым словом, те кому придётся всё переделывать?
          Что мешает работать с готовой HAL отделанной от регистров, и реализованной на обычном C или даже ассемблере где не надо бороться с проблемами вызванными улучшениями, призванными бороться с этими самыми проблемами но в общем виде?
            0
            По HAL, ну кроме того, что там оверхед — мама не горюй, хотя для меня это странно, если ты выпускаешь, например, 1 000 000 розеток умных в год и из-за использования HAL с оверхедом пришлось контроллер взять с большим ОЗУ и ПЗУ и дороже на 30 центов — это же 300 000 долларов и можно держать 3 программиста в США дополнительно и делать на один проект больше.

            Так вот кроме этого, я считаю, что его можно использовать только в устройствах, которые поработал быстро и выключил, типа CAN сканера для считывания ошибок в автомобиле, или управление геликоптером, или блутуз чайник (хотя спорный вопрос, но знаю, что Редмонд его и использует), где риск отказа вообще не высокий. Ведь не беда, что устройство работает час, а потом перестает отвечать через USB(баг в USB там жесткий был)… или вытыкаешь на живую, а windows с синим экраном падает, ну ничего пользователь просто переткнул устройство, перегрузил компьютер и все…

            А если вы делаете медицинское оборудования, где от такого зависания зависит жизнь человека? А если на атомной станции стоит?
            Я бы даже в умной розетке его бы не использовал, потому, что опасно это.
            Ответ на стековрелфлоу, мне кажется очень разумным:
            It gives pseudo developers false feeling that they do not have to know how their hardware works.
            Time spent learning HAL may be longer (and usually is) than needed to understand how the hardware works.
            Horrible overhead
            Many errors.

            А переделывать все равно придется рано или поздно, ведь Общедоступные библиотеки же появляются уже тот Kvasir, modm. А закрытых еще больше… и все уже написано и ethernet и usb и стеки все и оно не жрет столько ресурсов, как HAL и код даже компактнее, чем на ассемблере.
            Я вот помню, как примерно в таком же стиле переходили в 90х с ассемблера на Си. Ну тоже самое все было… и библиотеки и программисты только на ассемблере писали для 6800, 51 и PIC, но ведь перешли, ничего, сейчас кто ассемблер вспоминает?

            Даже ARM это понял что люди переходят на С++ и выпустил вот это для генерации нормальных регистров на С++.
              0
              Если не нравится готовые BSP вы можете сами написать модули работы с периферией и использовать их из C++. Будет проще и короче. Нафига тащить в выскоуровневый код особенности целевой платформы. Если вы хотите управлять светодиодом это должно выглядеть так:
              digitalWrite(led1Pin, HIGH);
              или так
              analogWrite(led2Pin, level);
                +1
                А чем это лучше, такого
                Led1::Set();
                А теперь представьте, что вам надо переключить сразу несколько ножек на разных портах… на Си это во сколько на ассемблере выльется?
                А на С++ в 5-7 команд. Смотрите Reflector показал, как это реализовано easyelectronics.ru/rabota-s-portami-vvoda-vyvoda-mikrokontrollerov-na-si.html
                А код будет выглядеть, примерно так
                LedList<<Led1, Led2, Led4, Led3>::Toggle();
                И всего то, и выродится это может в что-то типа
                GpioA->Odr ^= (1 <<1) | (1<<2);
                GpioC->Odr ^= (1 <<4) | (1<<3);

                У него даже еще лучше через BSRR регистры
                  0

                  В статье довольно простой поиск групп пинов, там ищутся только идущие подряд, только в одном направлении и не должно быть других пинов относящихся к тому-же порту, т.е. список из PA8, PA7, PA6, PA3 — это 4 отдельных пина, из-за последнего. У меня ищутся цепочки в обоих направлениях и порядок не имеет значения, для списка PA8, PA7, PA3, PA5 будет найдена цепочка PA8, PA7, PA5, потому что если биты 3, 2 и 0 входных данных сдвинуть на 5 влево, то они станут как раз битами 8, 7 и 5. И в статье старенькая Loki используется, я брал за основу списки типов от Олега Фатхиева, на вариадиках, есть видео на youtube .

                    0
                    Представите что вам вообще не нужно переключать ножки на C++ а просто выставляете код ошибки. А он отобразиться на 9-ти сегментном индикаторе, в виде моргания сетодиодов или последовательности звуковых сигналов в зависимости от платы под которую собран проект. Я говорю про то что часть которая зависит от платы должна быть не большой, простой, легко меняемой и независимой от вашей программы написанной на C++ которая реализует конкретный функционал. И тянуть в неё всякие излишества не имеет практического смысла.
                      +2

                      Есть у меня класс для работы с дисплеями, дисплеи могут быть с разными контролерами и работает это все через SPI, FMC или ногодрыг. В последнем случае я передают в шаблон отдельные управляющие пины(RS, WR...) и список пинов для данных, там они могут идти в любом порядке. Можно взять плату на которой дисплей с 16-ти битной шиной подключен к FMC и заставить ее работать через ногодрыг задав для данных такой список пинов:


                      PinList<PD10, PD9, PD8, PE15, PE14, PE13, PE12, PE11, PE10, PE9, PE8, PE7, PD1, PD0, PD15, PD14> LcdData;

                      Все, теперь внутри либы будет вызваться LcdData::write(...) и все разлирутся самом собой, пины автоматически разобьются на 4 группы и данные будут записаны в 2 порта, причем так же работает чтение, установка режима, быстрая смена направления и т.д… С точки зрения пользователя все предельно просто, эффективность на самом высоком уровне… С семисегментником будет то же самое, не важно к каким ногам он подключен, достаточно в списке расставить пины в правильном порядке.

                        0
                        Как раз все наоброт, на Си придется переписывать этот кусок, в зависимости от конфигурации порта, номера ножек и так деле, а на С++ берите и переиспользуйте его в другом проекте. И даже проще чем на Си.
                        А то что там на задворках для компилятора написано, в это вникать не надо… Это не для микроконтроллера, это для компилятора.
                        Кстати насчет ошибки… вы же вот тут написали Унифицированная обработка ошибок (C++ вариант для микроконтроллеров)
                        пример на Си — но это же столько ненужной работы надо сделать, и как его вообще переиспользовать, если у меня новые коды ошибок появляются? А на С++ ничего делать не надо… только список определи и все…
                        using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>;
                        И самое главное, в итоге по ресурсам процессора меньше. Зачем эти ненужные траты времени на написание скрипта? считайте что, аналог этого скрипта на С++ это как раз реализация класса метапрограммированием. Еще раз повторяюсь, все что там на шаблонах написано, это и есть скрипт-код не для микроконтроллера, а для того, чтобы компилятор этот код (скрипт) выполнил во время компиляции.
                          +1
                          Вы как-то предвзято относитесь к C на нём так же можно параметризовать к каким ногам привязано внешнее оборудование. Я про другое вы добавляете множество ненужных сущностей, якобы для упрощения. Если что-то не заработает то придётся разбираться с вашими задворками которые только для компилятора.
                          По поводу скриптов, вот у вас есть десяток вариантов плат. Где вы их конфигурации храните, даташиты и схемы по разводке? В шаблонах. Нет вы их храните в системе контроля версий в субмодуле платы. А на шаблонах там сделано или на дефайнах это дело десятое. Просто скриптом вытягиваете нужную ревизию и собираете. Причем заставляете это делать какой-нибудь jenkins.

                          «И самое главное, в итоге по ресурсам процессора меньше» — это не главное.
                          Напоминаю главное чтобы работало здесь и сечас. Поэтому переписывать так чтобы было офигенно со всех сторон как правило экономически не выгодно.
                            –1
                            Да я не имею ничего против Си, я согласен, что новый С18, вроде бы тоже штука мощная. Я Си не так хорошо знаю, собственно и С++ тоже не освоил до конца, там еще учить и учить. Но то, что применяю, кажется очень эффективным.
                            В общем я не хотел сказать, что Си плохой и на нем не надо писать, я к нему отношусь нейтрально, как к любому другому языку, который не очень знаю. Я больше хочу сказать, что С++ хороший и на нем можно писать эффективно на микроконтроллерах, но не обязательно нужно.
                            Думаю, что если надо быстро выпустить продукт на рынок широкого потребления, то да, взял Cube, HAL, быстро накидал логику — выпустил и собираешь прибыль, то альтернативе Си сейчас нет.
                            Но вот если продукт для атомной станции, который делается годами, то можно и потратить время на изучение более безопасного подхода, в том числе и на С++
                    +1
                    переход с ассма на си дал очень много,
                    но разница в сложности между си и асмом не такой большой была,
                    а учитывая сколько видов не совместимых с собой асмов на одну и ту же архитектуру было я бы за засомневался была ли сложность асма ниже чем у си, особенно у асмов с макросами и метапрограммингом, особенно у дсп от AD и техасов в конце 90ых — вот там творился вообще адский ад, пример попроще — турбоассемблер и макроассемблер под ДОС.
                    Пюсы дают проблемы компиляции на ровном месте из за более строгой типизации, зато огромный плюс плюсов в том что не перепутаешь xxxAPHxxxx и xxxAPBxxxx константные энумы инициализации и прочие плюшки строгой типизации и более развитый язык за который нужно платить в разы возрастающим временем проектирования, например генерики функций на шаблонах — такое порой проще препроцессором си написать (самое главное чтоб этот АД не пришлось потом через год поправить — никакая даже самая подробная дока не спасёт от траты минимум дня на разбор что это за ужас) — я про то что случае си читать потом неудобно, а в случае плюсов проектировать такое гораздо дольше — существенного преимущества нет.

                    Именно поэтому, я сомневаюсь, что плюсы полностью си вытеснят.
                    Скорее кончатся программисты которые на голом си умеют писать.
                      –2
                      Полностью согласен. Ни C ни asm никуда не денутся. А C++ надо применять не везде, а там где это оправдано не говоря уже о зоопарке новых стандартов.
                        0
                        Почти согласен, что проектировать новое всегда дольше, но как только спроектировал, перезаимствовать уже намного проще. Но поддерживать код приложения на С++ гораздо проще, потому что он просто понятнее…

                        И да, так и будет — программисты кончатся, как кончились программисты на чистом ассемблере. По-крайней мере, так как я преподаю в университете, а там только С++ дают, в том числе и мой курс для микроконтроллеров. Про указатели многие студенты, приходя ко мне на курс даже и не знают, про ссылки знают, про ссылки на rvalue знают. А указатель это новость для них. Может это и правильно, не будут не безопасный код писать.
                          +3
                          Про указатели многие студенты, приходя ко мне на курс, даже и не знают, про ссылки знают.

                          Юные, жизнерадостные идиоты...)))
                        0

                        Только настоящий безопасный код пишут на С. С++ для тех, кто переполз с буста или ещё чего подобного.

                          +1
                          Только настоящий безопасный код пишут на С.

                          На C++ не пишут? Какие же возможности С помогают писать настоящий безопасный код?


                          С++ для тех, кто переполз с буста или ещё чего подобного.

                          Но ведь буст — набор библиотек для C++, написанный на C++.

                      0
                      Только про простое ногодрыжество? А как же SFR, реализовать их типобезопасно интереснее, нет?
                        0
                        Я рассматривал только периферийные регистры, потому что они часто используются в разных модулях и следовательно надо их передавать как-то. А регистры специального назначения, по моему мнению, нужны только при начальной настройке и при реализации РТОС. Так то их трогать не рекомендуется. Но вообще АРМ делает такие вот файлики и можно от производителя его получить и генерить регистры по адресам. Считайте способ 8 или 7 будет…
                        +1
                        Заинтересовался данной статьей, так как сейчас занимаюсь написанием библиотек для stm32mp15x. Статью прочитал несколько раз, вдумчиво, заметил следующий минус:
                        Тип доступа RO/WO/RW должен отностится не к регистру, а строго к каждому биту в отдельности
                        Дело в том, что во многих регистрах соседствуют биты rw и ro, некоторые биты сбрасываются записью 1, а не 0. Отсюда минус «можно сделать глупость» никуда не уходит.
                        В заголовочниках от производителя нет описания типов доступа для отдельных бит, а значит автоматически пересобрать заголовок с регистрами через python не получится. Теоретически это может делать производитель путем обработки исходных hdl-кодов (собственно, стандартные cmsis-заголовочники так и делаются), но вряд ли они будут делать это в сколь-нибудь обозримом будущем.
                          0
                          Вот тут я немного поигрался… www.onlinegdb.com/edit/BJ7BiHDqE
                          Там идея в том, в том, что генерация описания классов и регистров будет из SVD файлов, а код обращения к регистрам уже не сложный.
                            0
                            И да, некоторые производители делают описание для перечислений, т.е. допустимых значений битов с их именами. Например, Техас, но ST почему то игнорирует это. Хотя с ST можно договориться :) если надо. Но самому для каждого бита добавлять тип только для чтения или только на запись по 1, например, это конечно еще та работенка. В любом случае, можно по возможности ограничить запись неверных значений, или вообще ограничить запись сразу всего регистра, а предоставить доступ только для конкретных битов.
                            Если время будет, я попытаюсь это описать и выложить на хабр.
                              +1
                              Куда хуже когда производители вообще не дают описание периферии.

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

                          Самое читаемое