Продолжение очередной статьи: STM32 для начинающих. Интерфейсы

    Предыдущая публикация: «Очередная статья — STM32 для начинающих»

    И как этим пользоваться?


    В предыдущей статье создали класс для работы с портами ввода-вывода, проверили. И что дальше? Зачем это все запихивать в класс?

    Возьмем для примера простенький опрос кнопок:


    Для этой схемы в простейшем случае опрос будет выглядеть так:

    int GetKey()
    {
      volatile uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR);
      uint32_t ret_val = *addr;
      return ret_val & 0x0F;
    }

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

    Перепишем эту функцию под ранее созданный класс:

    int GetKey(Pin* p0, Pin* p1, Pin* p2, Pin* p3)
    {
      int ret_val = p0->Get() + (p1->Get() << 1) + (p2->Get() << 2) + (p3->Get() << 3);
      return ret_val;
    }

    Остается в главной программе инициализировать порты и передать в функцию:

    ...
    using namespace STM32F1xx;
    Pin key0('a', 0);
    Pin key1('a', 1);
    Pin key2('a', 2);
    Pin key3('a', 3);
    ...
    int main()
    {
      key0.ModeInput();
      key1.ModeInput();
      key2.ModeInput();
      key3.ModeInput();
      int key_code = GetKey(&key0, &key1, &key2, &key3);
    ...
      return 0;
    }
    

    А где же интерфейсы?


    А теперь представим, законились контроллеры серии f10x, но есть куча f030. По производительности и количеству выводов хватает, только надо хедер поменять для функции GetKey или воспользоваться… #ifdef. Сделать глобальный заголовочный файл, в котором прописть тип используемого контроллера (что то типа #define STM32F030) и нагородить кучу дефайнов. Нет, не для этого создавались языки высокого уровня, что бы путаться в макросах!

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

    iPin.h
    #pragma once
    
    class iPin
    {
    public:
      virtual void ModeInput()              = 0;
      virtual void ModeAnalogInput()        = 0;
      virtual void ModeInputPulled()        = 0;
      virtual void ModeOutput()             = 0;
      virtual void ModeOutputOpenDrain()    = 0;
    
      virtual void Set(bool st) = 0;
      virtual bool Get() = 0;
    
      virtual void Reverse() { Set(!Get());}
    
      void On()              { Set(true);  }
      void Off()             { Set(false); }
    };


    (те методы, которые приравнены 0, должны быть определены в производном классе!)
    и будем его использовать как базовый в классе Pin:

    ...
    #include "iPin.h"
    ...
    class Pin : public iPin
    ...

    тогда функция GetKey чуть изменится:

    int GetKey(iPin* p0, iPin* p1, iPin* p2, iPin* p3)
    {
      int ret_val = p0->Get() + (p1->Get() << 1) + (p2->Get() << 2) + (p3->Get() << 3);
      return ret_val;
    }
    

    Теперь нам любой контроллер нипочем! Даже если это шинный расширитель, работающий по SPI или I2C. Последовательные интерфейсы рассмотрим в следующих статьях.

    И что дальше?


    Дальше надо оформить класс для работы с системным таймером. Но это уже в следующей публикации.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

      +1
      Это вы изобрели digitalWrite / digitalRead из Ардуины? И вместо одного такта на запись регистра тратите десяток на разыменования указателей, сравнения и т.п.
      Поскольку назначение выводов во время работы кода не меняется, их можно задефайнить:
      #define BTN1 A, 4, 1, GPIO_PULL //PA4, активный уровень лог.1, подтяжка вниз
      GPIO_config( BTN1 );
      if( GPI_ON( BTN1 ) ){...}

      И соответственно макросы ( ):
      #define concat2(a,b,...)  a##b
      #define concat3(a,b,c,...)  a#b#c
      
      #define marg1(a,...)  a
      #define marg2(a,b,...)  b
      #define marg3(a,b,c,...)  c
      #define marg4(a,b,c,d,...)  d
      #define GPIO(x) concat2(GPIO,x)
      
      #define _GPIO_CONFIG(port, bit, letter, mode) \
        do{\
          uint32_t temp = GPIO##port -> concat2(CR,letter);\
          temp &=~(0b1111<<(((bit)&0x07)*4)); \
          temp |= ( mode <<(((bit)&0x07)*4)); \
          GPIO##port -> concat2(CR,letter) = temp; \
        }while(0)
      
        
      #define GPIO_mode(port, bit, mode)\
        do{ \
          if(bit<8)_GPIO_CONFIG(port,bit,L,mode); \
            else _GPIO_CONFIG(port,bit-8,H,mode); \
        }while(0)
      #define GPIO_config(descr) \
        do{ \
          GPIO_mode(marg1(descr), marg2(descr), marg4(descr)); \
          if(marg4(descr) == GPIO_PULL){ \
            if(marg3(descr))GPIO(marg1(descr))->BRR = (1<<marg2(descr)); \
              else GPIO(marg1(descr))->BSRR = (1<<marg2(descr)); \
          } \
        }while(0)
      
      #define GPO_ON(descr) \
        do{ \
          GPIO(marg1(descr))->BSRR = (1<<marg2(descr)<<((1-marg3(descr))*16)); \
        }while(0)
      #define GPO_OFF(descr) \
        do{ \
          GPIO(marg1(descr))->BSRR = (1<<marg2(descr)<<((marg3(descr))*16)); \
        }while(0)
      #define GPO_T(descr) \
        do{ \
          GPIO(marg1(descr))->ODR ^= (1<<marg2(descr)); \
        }while(0)
      #define GPI_ON(descr)  ((GPIO(marg1(descr))->IDR & (1<<marg2(descr)))==(marg3(descr)<<marg2(descr)))
      #define GPI_OFF(descr) ((GPIO(marg1(descr))->IDR & (1<<marg2(descr)))!=(marg3(descr)<<marg2(descr)))
      

      Не спорю, макросы выглядят страшно, но менять их надо только при переходе на другое семейство. Точнее, даже не менять, а переписывать, поскольку там другая логика работы портов
        +2

        Да вроде на разыменовывание не тратится ничего… если вы про это...


        int GetKey()
        {
        volatile uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR);
        uint32_t ret_val = *addr;
        return ret_val & 0x0F;
        }

        то все можно заменить на одну строчку без накладных.


        inline int GetKey()
        {
          return (*reinterpret_cast<volatile uint32_t*>(GPIOA_IDR)) & 0x0F;
        }

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


        auto GetKey(const iPin& p0, const iPin& p1, const iPin& p2, const iPin& p3)
        {
            const auto ret_val = p0.Get() | (p1.Get() << 1) | (p2.Get() << 2) | (p3.Get() << 3);
            return ret_val;
        }

        Проблема еще в том, что порты считывается в разный момент времени… а желательно бы одновременно состояние порта считывать, а то пока p0.Get() считается, уже p3.Get() может поменяться… — это конечно, возможно, надумано, но вдруг на низкой частоте все работает или в окружении Операционной системы, прервали вас на 40 мс высокоприоритетной задачей и, вуаля, считали неправильное нажатие кнопки.


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


        using KeyPins = PinsList<p1,p2,p3,p4>;
        //.. либо в структуру, если с темплейтами неохота заморачиваться..
        struct KeyPins
        {
          iPin& p1;
          iPin& p2;
          iPin& p3;
          iPin& p4;
        } 

        И потом в одном месте её проинициализировать. Если и будет ошибка, то исправить её можно быстро в одном месте, а не бегать по коду и искать, где вызывается метод GetKey()


        Да еще добавлю. Так делать не совсем хорошо:


        virtual void Reverse() { Set(!Get());}

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

          0
          то все можно заменить на одну строчку без накладных.

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

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

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

          это был как пример. конечно, лучше списком или структурой или подобным.
          Да еще добавлю. Так делать не совсем хорошо:
          virtual void Reverse() { Set(!Get());}

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

          хорошее замечание.
          спасибо.
          +1
          Это я привел пример только для пинов. Без прерываний. Боюсь спросить как будет тотже уарт выглядеть на дефайнах. Особенно инетересен обработчик.
          Макросы тоже не плохи, раньше пользовался подобным. Но вот предложил другой подход.
            0
            А че там обрадаьывать-то? Если у нас есть DMA, то работа с уартом не сильно отличается от работы с массивом в памяти. Ну а сконфигурировать его макросами не составляет большого труда.

            Ну и в принципе на уровне железа макросы не сильно отличаются от инлайнов. Делать же набор функций чтобы дергать ножкой это правда жир, кушающий и такты и стек. Можно, конечно, надеяться, что компилятор соптимизирует.
              0
              А че там обрадаьывать-то? Если у нас есть DMA, то работа с уартом не сильно отличается от работы с массивом в памяти. Ну а сконфигурировать его макросами не составляет большого труда.

              В теории да. У меня как раз проблема при использовании DMA — как организовать циклический буфер? Как раз в библиотеке ModBus хочется прикрутить DMA.
              Делать же набор функций чтобы дергать ножкой это правда жир, кушающий и такты и стек.

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

              так ведь для этого и существуют компиляторы.
                0
                У stm32, вроде, есть Circular Mode. (1.1.8 Transfer types)
              0
              Использование примерно такое:
              UART_init(1,32000000/9600);
              ...
              UART1_puts("Test");
              if(UART_rx_count(1)!=0){
                  if(UART_getc(1) == 'v'){GPO_T(GLED);}
              }

              Если интересно, выложу и макросы.
              а Вы замеряли сколько тактов теряется? при максимальной оптимизации я не заметил особой разницы.

              Ну, если бы вы выложили полный проект, готовый для компиляции, можно было бы и сравнить. Мой вариант на макросах разворачивается в 1 ассемблерную команду для GPO_ON / GPO_OFF, примерно столько же на проверки.
                0

                Да, конечно интересно. Всегда интересно увидеть разные подходы.
                "Ну, если бы вы выложили полный проект, готовый для компиляции..."
                Это относилось к виртуальным методам. Бесспорно, макросам такой подход в проигрыше по производительности. Навскидку, будет 6...7 тактов. За все приходится платить.

                  0
                  Вот реализация UART для stm32l151 если и правда интересно. Изначально задумывалось только как отладочный интерфейс, поэтому над оформлением особо не заморачивался:
                  github.com/COKPOWEHEU/stm32modules/blob/master/L151/uart.h
            0
            А если копнуть чуть глубже, то окажется что есть отдельные регистры чтения, записи, и атомарной установки контактов порта. И всё что требуется — так научится с ними работать.
              +1
              Фрагмент кода:
              int GetKey()
              {
                volatile uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR);
                uint32_t ret_val = *addr;
                return ret_val & 0x0F;


              Выглядит немного странным: функция возвращает int (знаковое), а return использует uint.
                –1
                Название не совсем адекватное на мой взгляд. Должно быть не «STM32 для начинающих», а «Набор скилов для того, чтобы быть принятым в команду такого-то синьора на позицию джун». Начинающие, если они в здравом уме, идут на сайт производителя этих микроконтроллеров и следуют рекомендациям производителя, каковые существенно отличаются от тех, что изложены в этих двух статьях. Начинающие осваивают внутренности микроконтроллера, долго и нудно, и с использованием самых примитивных конструкций языка С. Embedded чувак — он в первую очередь должен быть EE — спец по электронике, а продвинутое программирование — это потом.

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

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