Очередная статья: STM32 для начинающих

    Всех приветствую!

    Это моя первая статья на Хабре, поэтому прошу не кидаться тяжелыми предметами. Заранее спасибо.

    Начнем с предыстории. Когда-то мне пришлось перейти на микроконтроллеры ARM фирмы ST. Это было связано с тем, что PIC и AVR уже не хватало и хотелось новых приключений. Из доступного в хлебобулочных магазинах и большого количества статей о «быстром старте» выбор пал именно на STM32F100.

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

    Когда я попытался сделать первый проект меня ждало разочарование — CMSIS! Кому как, но для меня это было (и остается) ужасом: много буков, длинные и для меня не понятные структуры. Вникать во все это было не интересно. Попытался скомпилировать пару примеров и понял — это не наш метод.

    Неужели нет других вариантов? Есть. Тот, встроенный в IAR: iostm32f10xx4.h и подобные инклудники. Вполне не плохо:

    RCC_APB2ENR_bit.ADC1EN = 1; // включить тактирование ADC

    Оставалось это запихнуть в классы и пользоваться. Так и сделал. Через какое-то время потребовалось сделать код для STM32f4xx. И тут снова засада — нет инклудиков. Что делать? — писать самому. Проанализировал имеющиеся самописные библиотеки решил немного сделать по другому. Вот об этом и будет рассказ.

    Начало


    Про установку IAR и драйверов для отладчика рассказывать не буду, т.к. здесь ничего нового. У меня стоит IAR 8 с ограниченем кода в 32кБ. Для работы выбран контроллер STM32F103, установленный на плате plue pill.

    Запускаем IAR, создаем проект c++, выбираем нужный контроллер
    image
    Следующий шаг — изучение документации. Нас будет интересовать Reference manual RM0008. Там главное внимательно читать.

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

    Модуль RCC. Такирование


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

    Запомните! Что бы включить какую-либо периферию, на нее надо подать тактовые импульсы! Без этого никак.

    Порты ввода-вывода сидят на шине APB2. Находим в документации регист для упрвления тактированием этой шины, это RCC_APB2ENR:



    Чтобы включить тактирование порта C (светодиод как раз припаян к PC13), требуется записать в бит IOPCEN единичку.

    Теперь найдем адрес регистра RCC_APB2ENR. Смещение у него 0x18, базовый адрес для регистров RCC 0x40021000.

    Чтобы удобно было работать с битами, создадим структуру:

    typedef struct
    {
      uint32_t  AFIOEN         : 1;
      uint32_t                 : 1;
      uint32_t  IOPAEN         : 1;
      uint32_t  IOPBEN         : 1;
      uint32_t  IOPCEN         : 1;
      uint32_t  IOPDEN         : 1;
      uint32_t  IOPEEN         : 1;
      uint32_t                 : 2;
      uint32_t  ADC1EN         : 1;
      uint32_t  ADC2EN         : 1;
      uint32_t  TIM1EN         : 1;
      uint32_t  SPI1EN         : 1;
      uint32_t                 : 1;
      uint32_t  USART1EN       : 1;
      uint32_t                 :17;
    } RCC_APB2ENR_b;
    

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

    enum AddrRCC
    {
      RCC_CR          = 0x40021000,
      RCC_CFGR        = 0x40021004,
      RCC_CIR         = 0x40021008,
      RCC_APB2RSTR    = 0x4002100C,
      RCC_APB1RSTR    = 0x40021010,
      RCC_AHBENR      = 0x40021014,
      RCC_APB2ENR     = 0x40021018,
      RCC_APB1ENR     = 0x4002101C,
      RCC_BDCR        = 0x40021020,
      RCC_CSR         = 0x40021024
    };
    

    теперь остается написать код для включения периферии:

    static void EnablePort(uint8_t port_name)
    {
      volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
      switch (port_name)
      {
        case 'A': apb2enr->IOPAEN = 1; break;
        case 'a': apb2enr->IOPAEN = 1; break;
        case 'B': apb2enr->IOPBEN = 1; break;
        case 'b': apb2enr->IOPBEN = 1; break;
        case 'C': apb2enr->IOPCEN = 1; break;
        case 'c': apb2enr->IOPCEN = 1; break;
        case 'D': apb2enr->IOPDEN = 1; break;
        case 'd': apb2enr->IOPDEN = 1; break;
        case 'E': apb2enr->IOPEEN = 1; break;
        case 'e': apb2enr->IOPEEN = 1; break;
      }
    }
    

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

    Тоже самое делаем для включения тактирвания другой периферии.

    В итоге получился такой класс (не все перечислено):

    STM32F1xx_RCC.h
    
    #pragma once
    #include "stdint.h"
    namespace STM32F1xx
    {
      class RCC
      {
      protected:
        enum AddrRCC
        {
          RCC_CR          = 0x40021000,
          RCC_CFGR        = 0x40021004,
          RCC_CIR         = 0x40021008,
          RCC_APB2RSTR    = 0x4002100C,
          RCC_APB1RSTR    = 0x40021010,
          RCC_AHBENR      = 0x40021014,
          RCC_APB2ENR     = 0x40021018,
          RCC_APB1ENR     = 0x4002101C,
          RCC_BDCR        = 0x40021020,
          RCC_CSR         = 0x40021024
        };
        
        typedef struct {
          uint32_t  HSION          : 1;
          uint32_t  HSIRDY         : 1;
          uint32_t                 : 1;
          uint32_t  HSI_TRIM       : 5;
          uint32_t  HSI_CAL        : 8;
          uint32_t  HSEON          : 1;
          uint32_t  HSERDY         : 1;
          uint32_t  HSEBYP         : 1;
          uint32_t  CSSON          : 1;
          uint32_t                 : 4;
          uint32_t  PLLON          : 1;
          uint32_t  PLLRDY         : 1;
          uint32_t                 : 6;
        } RCC_CR_b;
    		
        typedef struct {
          uint32_t  SW             : 2;
          uint32_t  SWS            : 2;
          uint32_t  HPRE           : 4;
          uint32_t  PPRE1          : 3;
          uint32_t  PPRE2          : 3;
          uint32_t  ADC_PRE        : 2;
          uint32_t  PLLSRC         : 1;
          uint32_t  PLLXTPRE       : 1;
          uint32_t  PLLMUL         : 4;
          uint32_t  USBPRE         : 1;
          uint32_t                 : 1;
          uint32_t  MCO            : 3;
          uint32_t                 : 5;
        } RCC_CFGR_b;
    
        typedef struct
        {
          uint32_t  TIM2EN         : 1;
          uint32_t  TIM3EN         : 1;
          uint32_t  TIM4EN         : 1;
          uint32_t                 : 8;
          uint32_t  WWDGEN         : 1;
          uint32_t                 : 2;
          uint32_t  SPI2EN         : 1;
          uint32_t                 : 2;
          uint32_t  USART2EN       : 1;
          uint32_t  USART3EN       : 1;
          uint32_t                 : 2;
          uint32_t  I2C1EN         : 1;
          uint32_t  I2C2EN         : 1;
          uint32_t  USBEN          : 1;
          uint32_t                 : 1;
          uint32_t  CANEN          : 1;
          uint32_t                 : 1;
          uint32_t  BKPEN          : 1;
          uint32_t  PWREN          : 1;
          uint32_t                 : 3;
        } RCC_APB1ENR_b;
    		
        typedef struct
        {
          uint32_t  AFIOEN         : 1;
          uint32_t                 : 1;
          uint32_t  IOPAEN         : 1;
          uint32_t  IOPBEN         : 1;
          uint32_t  IOPCEN         : 1;
          uint32_t  IOPDEN         : 1;
          uint32_t  IOPEEN         : 1;
          uint32_t                 : 2;
          uint32_t  ADC1EN         : 1;
          uint32_t  ADC2EN         : 1;
          uint32_t  TIM1EN         : 1;
          uint32_t  SPI1EN         : 1;
          uint32_t                 : 1;
          uint32_t  USART1EN       : 1;
          uint32_t                 :17;
        } RCC_APB2ENR_b;
    
        typedef struct {
          uint32_t  DMAEN          : 1;
          uint32_t                 : 1;
          uint32_t  SRAMEN         : 1;
          uint32_t                 : 1;
          uint32_t  FLITFEN        : 1;
          uint32_t                 : 1;
          uint32_t  CRCEN          : 1;
          uint32_t                 :25;
        } RCC_AHBENR_r;
        
      public:
        static void EnablePort(uint8_t port_name)
        {
          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
          switch (port_name)
          {
            case 'A': apb2enr->IOPAEN = 1; break;
            case 'a': apb2enr->IOPAEN = 1; break;
            case 'B': apb2enr->IOPBEN = 1; break;
            case 'b': apb2enr->IOPBEN = 1; break;
            case 'C': apb2enr->IOPCEN = 1; break;
            case 'c': apb2enr->IOPCEN = 1; break;
            case 'D': apb2enr->IOPDEN = 1; break;
            case 'd': apb2enr->IOPDEN = 1; break;
            case 'E': apb2enr->IOPEEN = 1; break;
            case 'e': apb2enr->IOPEEN = 1; break;
          }
        }
    
        static void DisablePort(char port_name)
        {
          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
          switch (port_name)
          {
            case 'A': apb2enr->IOPAEN = 0; break;
            case 'a': apb2enr->IOPAEN = 0; break;
            case 'B': apb2enr->IOPBEN = 0; break;
            case 'b': apb2enr->IOPBEN = 0; break;
            case 'C': apb2enr->IOPCEN = 0; break;
            case 'c': apb2enr->IOPCEN = 0; break;
            case 'D': apb2enr->IOPDEN = 0; break;
            case 'd': apb2enr->IOPDEN = 0; break;
            case 'E': apb2enr->IOPEEN = 0; break;
            case 'e': apb2enr->IOPEEN = 0; break;
          }
        }
    
        static void EnableAFIO()
        {
          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
          apb2enr->AFIOEN = 1;
        }
    
        static void DisableAFIO()
        {
          volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
          apb2enr->AFIOEN = 0;
        }
        
        static void EnableI2C(int PortNumber)
        {
          switch (PortNumber)
          {
            case 1:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->I2C1EN = 1;
              break;
            }
            case 2:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->I2C2EN = 1;
              break;
            }
    
          }
        }
    
        static void EnableUART(int PortNumber)
        {
          switch (PortNumber)
          {
            case 1:
            {
              volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
              apb2enr->USART1EN = 1;
              break;
            }
            case 2:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->USART2EN = 1;
              break;
            }
            case 3:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->USART3EN = 1;
              break;
            }
    
          }
        }
        
        static void DisableUART(int PortNumber)
        {
          switch (PortNumber)
          {
            case 1:
            {
              volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
              apb2enr->USART1EN = 0;
              break;
            }
            case 2:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->USART2EN = 0;
              break;
            }
            case 3:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->USART3EN = 0;
              break;
            }
          }
        }
        
        static void EnableSPI(int PortNumber)
        {
          switch (PortNumber)
          {
            case 1:
            {
              volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
              apb2enr->SPI1EN = 1;
              break;
            }
            case 2:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->SPI2EN = 1;
              break;
            }
          }
        }
    
        static void DisableSPI(int PortNumber)
        {
          switch (PortNumber)
          {
            case 1:
            {
              volatile RCC_APB2ENR_b* apb2enr = reinterpret_cast<RCC_APB2ENR_b*>(RCC_APB2ENR);
              apb2enr->SPI1EN = 0;
              break;
            }
            case 2:
            {
              volatile RCC_APB1ENR_b* apb1enr = reinterpret_cast<RCC_APB1ENR_b*>(RCC_APB1ENR);
              apb1enr->SPI2EN = 0;
              break;
            }
          }
        }
        
        static void EnableDMA()
        {
          volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);
          ahbenr->DMAEN = 1;
        }
        
        static void DisableDMA()
        {
          volatile RCC_AHBENR_r* ahbenr = reinterpret_cast<RCC_AHBENR_r*>(RCC_AHBENR);
          ahbenr->DMAEN = 0;
        }
      };
    }
    


    Теперь можно в main.cpp присоединить файл и пользоваться:

    #include "STM32F1xx_RCC.h"
    
    using namespace STM32F1xx;
    
    int main()
    {
      RCC::EnablePort('c');
      return 0;
    }
    

    Теперь можно и с портами поработать. GPIO


    Открываем в документации раздел General-purpose and alternate-function I/Os. Находим Port bit configuration table:



    Битами CNF[1:0] задается режим работы порта (аналоговый вход, цифровой вход, выход), биты MODE[1:0] отвечат за скорость работы порта в режиме выход.

    Взглянем на регистры GPIOx_CRL и GPIOx_CRH (x=A, B, C,...)



    видно, что биты идут последовательно:

    CNF[1:0], MODE[1:0]

    тогда создадим константы с режимами работы портов

    enum mode_e
    {
      ANALOGINPUT             = 0,
      INPUT                   = 4,
      INPUTPULLED             = 8,
    
      OUTPUT_10MHZ            = 1,
      OUTPUT_OD_10MHZ         = 5,
      ALT_OUTPUT_10MHZ        = 9,
      ALT_OUTPUT_OD_10MHZ     = 13,
    
      OUTPUT_50MHZ            = 3,
      OUTPUT_OD_50MHZ         = 7,
      ALT_OUTPUT_50MHZ        = 11,
      ALT_OUTPUT_OD_50MHZ     = 15,
    
      OUTPUT_2MHZ             = 2,
      OUTPUT_OD_2MHZ          = 6,
      ALT_OUTPUT_2MHZ         = 10,
      ALT_OUTPUT_OD_2MHZ      = 14,
    
      OUTPUT                  = 3,
      OUTPUT_OD               = 7,
      ALT_OUTPUT              = 11,
      ALT_OUTPUT_OD           = 15
    };

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

    // pin_number - номер порта
    void Mode(mode_e mode)
    {
      uint32_t* addr;
      if(pin_number > 7)
        addr = reinterpret_cast<uint32_t*>(GPIOA_CRH);
      else
        addr = reinterpret_cast<uint32_t*>(GPIOA_CRL);
      
      int bit_offset;
      if(pin_number > 7)
        bit_offset = (pin_number - 8) * 4;
      else
        bit_offset = pin_number * 4;
    
      uint32_t mask = ~(15 << bit_offset);
      *addr &= mask;
      *addr |= ((int)mode) << bit_offset;
    }

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

        void ModeInput()              { Mode(INPUT);         }
        void ModeAnalogInput()        { Mode(ANALOGINPUT);   }
        void ModeInputPulled()        { Mode(INPUTPULLED);   }
        void ModeOutput()             { Mode(OUTPUT);        }
        void ModeOutputOpenDrain()    { Mode(OUTPUT_OD);     }
        void ModeAlternate()          { Mode(ALT_OUTPUT);    }
        void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }

    В документации находим адреса управляющих регистров для портов и перечислим:

    enum AddrGPIO
    {
      PortA           = 0x40010800,
      GPIOA_CRL       = 0x40010800,
      GPIOA_CRH       = 0x40010804,
      GPIOA_IDR       = 0x40010808,
      GPIOA_ODR       = 0x4001080C,
      GPIOA_BSRR      = 0x40010810,
      GPIOA_BRR       = 0x40010814,
      GPIOA_LCKR      = 0x40010818,
      PortB           = 0x40010C00,
      PortC           = 0x40011000,
      PortD           = 0x40011400,
      PortE           = 0x40011800,
      PortF           = 0x40011C00,
      PortG           = 0x40012000
    };

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

    Модернизируем метод:

    if(pin_number > 7)
      addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
    else
      addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);

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

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

    void Set(bool st)
    {
      uint32_t* addr;
      addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
    
      if(st)
        *addr |= 1 << pin_number;
      else
      {
        int mask = ~(1 << pin_number);
        *addr &= mask;
      } 
    }

    Также для управления состоянием можно воспользоваться регистрами GPIOx_BSRR.
    По аналогии делаем методы для считывания состояния порта, методы для конфигурации и инициализации (не забываем включить тактирование). В итоге получился такой класс для работы с портами:

    STM32F1xx_Pin.h
    #pragma once
    #include <stdint.h>
    #include "STM32F1xx_RCC.h"
    
    namespace STM32F1xx
    {
      class Pin
      {
      public:
        enum mode_e
        {
          ANALOGINPUT             = 0,
          INPUT                   = 4,
          INPUTPULLED             = 8,
    
          OUTPUT_10MHZ            = 1,
          OUTPUT_OD_10MHZ         = 5,
          ALT_OUTPUT_10MHZ        = 9,
          ALT_OUTPUT_OD_10MHZ     = 13,
    
          OUTPUT_50MHZ            = 3,
          OUTPUT_OD_50MHZ         = 7,
          ALT_OUTPUT_50MHZ        = 11,
          ALT_OUTPUT_OD_50MHZ     = 15,
    
          OUTPUT_2MHZ             = 2,
          OUTPUT_OD_2MHZ          = 6,
          ALT_OUTPUT_2MHZ         = 10,
          ALT_OUTPUT_OD_2MHZ      = 14,
    
          OUTPUT                  = 3,
          OUTPUT_OD               = 7,
          ALT_OUTPUT              = 11,
          ALT_OUTPUT_OD           = 15
        };
        
      private:
        enum AddrGPIO
        {
          PortA           = 0x40010800,
          GPIOA_CRL       = 0x40010800,
          GPIOA_CRH       = 0x40010804,
          GPIOA_IDR       = 0x40010808,
          GPIOA_ODR       = 0x4001080C,
          GPIOA_BSRR      = 0x40010810,
          GPIOA_BRR       = 0x40010814,
          GPIOA_LCKR      = 0x40010818,
          PortB           = 0x40010C00,
          PortC           = 0x40011000,
          PortD           = 0x40011400,
          PortE           = 0x40011800,
          PortF           = 0x40011C00,
          PortG           = 0x40012000
        };
        
      private:
        int   pin_number;
        int   PortAddr;
        
      public:
        Pin()                               { }
        Pin(char port_name, int pin_number) { Init(port_name, pin_number); }
        ~Pin()
        {
          Off();
          ModeAnalogInput();
        }
      public:
        void Init(char port_name, int pin_number)
        {
          this->pin_number = pin_number;
          RCC::EnablePort(port_name);
          switch (port_name)
          {
            case 'A': PortAddr = PortA; break;
            case 'a': PortAddr = PortA; break;
            case 'B': PortAddr = PortB; break;
            case 'b': PortAddr = PortB; break;
            case 'C': PortAddr = PortC; break;
            case 'c': PortAddr = PortC; break;
            case 'D': PortAddr = PortD; break;
            case 'd': PortAddr = PortD; break;
            case 'E': PortAddr = PortE; break;
            case 'e': PortAddr = PortE; break;
          }
        }
    
        void ModeInput()              { Mode(INPUT);         }
        void ModeAnalogInput()        { Mode(ANALOGINPUT);   }
        void ModeInputPulled()        { Mode(INPUTPULLED);   }
        void ModeOutput()             { Mode(OUTPUT);        }
        void ModeOutputOpenDrain()    { Mode(OUTPUT_OD);     }
        void ModeAlternate()          { Mode(ALT_OUTPUT);    }
        void ModeAlternateOpenDrain() { Mode(ALT_OUTPUT_OD); }
    
        void NoPullUpDown()
        {
          uint32_t* addr;
          if(pin_number > 7)
            addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
          else
            addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
    
          int bit_offset;
          if(pin_number > 7)
            bit_offset = (pin_number - 8) * 4;
          else
             bit_offset = pin_number * 4;
    
          int mask = ~((1 << 3) << bit_offset);
          *addr &= mask;
        }
        
        void Mode(mode_e mode)
        {
          uint32_t* addr;
          if(pin_number > 7)
            addr = reinterpret_cast<uint32_t*>(GPIOA_CRH - PortA + PortAddr);
          else
            addr = reinterpret_cast<uint32_t*>(GPIOA_CRL - PortA + PortAddr);
          
          int bit_offset;
          if(pin_number > 7)
            bit_offset = (pin_number - 8) * 4;
          else
            bit_offset = pin_number * 4;
    
          uint32_t mask = ~(15 << bit_offset);
          *addr &= mask;
          *addr |= ((int)mode) << bit_offset;
        }
    
        void Set(bool st)
        {
          uint32_t* addr;
          addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
    
          if(st)
            *addr |= 1 << pin_number;
          else
          {
            int mask = ~(1 << pin_number);
            *addr &= mask;
          } 
        }
    
        void On()
        {
          uint32_t* addr;
          addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
          int bit_offset = pin_number;
          *addr |= 1 << bit_offset;
        }
    
        void Off()
        {
          uint32_t* addr;
          addr = reinterpret_cast<uint32_t*>(GPIOA_ODR - PortA + PortAddr);
          int bit_offset = pin_number;
          int mask = ~(1 << bit_offset);
          *addr &= mask;
        }
    
        bool Get()
        {
          uint32_t* addr = reinterpret_cast<uint32_t*>(GPIOA_IDR - PortA + PortAddr);
          int bit_offset = pin_number;
          int mask = (1 << bit_offset);
          bool ret_val = (*addr & mask);
          return ret_val;
        }
      };
    };
    


    Ну что, опробуем:
    #include "STM32F1xx_Pin.h"
    
    using namespace STM32F1xx;
    
    Pin led('c', 13);
    
    int main()
    {
      led.ModeOutput();
      led.On();
      led.Off();
      return 0;
    }
    

    Проходим дебагером и убеждаемся, что светодиод сначала загорается (после led.ModeOutput();), потом гаснет (led.On();) и снова загорается (led.Off();). Это связано с тем, что светодиод подключен к ножке через линию питания. Поэтому, когда на выводе низкий уровень, светодиод загорается.

    Не большие итоги


    В данной статье я попытался (надеюсь, получилось) показать как можно немного упростить себе жизнь, сделать код более читаемым. Или наоборот — как нельзя делать. Каждый решит сам.
    Можно было просто написать враперы для CMSIS, но это не интересно.

    Спасибо за уделенное время. Если интересно продолжение — дайте знать.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

      +5
      Мне кажется, что stm32 вполне заслуживает более интересной работы, чем мигание светодиодом. Это конечно уважаемая, исторически сложившаяся, традиционная так, сказать работа для микроконтроллеров, но хотелось бы чего-нибудь посвежее.
      А так, да добро пожаловать в клуб любителей STM -ок на ХАБРе.
        0
        Спасибо, это только начало.
          +3
          даже на младших STM32 которые запитаны от часовой батарейки можно сделать плавный классный интерфейс с анимированными свайпами, да разрешение будет не VGA и цветов очень мало но он будет летать!
          а на старшие STM32 вполне успевают обрабатывать видео налету и принимать решения.
          топовые же STM32 тянут полновесные CNN сетки типа MobileNetV2 и говорить можно не о секундах на кадр а о FPS, естественно всё в реалтайме.
          Но дальше мигания светодиода, почему то почти никто не рассказывает.
          Обидно. Мне кажется что у многих «аллергия на мигание светодиодом» просыпается.

          Я бы для стартового проекта поднял бы уарт. Почему? Любой язык начинается с Hello world а для этого нужен printf, но для printf нужен uart. Если есть хотя-бы он то всё остальное сделать в разы проще. Я всегда начинаю с уарта и printf/scanf, будь то FPGA, MCU или малинка в baremethal. Более того простой printf/scanf можно вполне впихнуть в пару тыщь байт или LUT/reg ячеек. Т.е. он влезет КУДА УГОДНО и ещё останется чтоб поднять и наладить любую другую периферию. А подымать и настраивать периферию удобнее если ты можешь в реалтайме выводить события, состояния регистров в бинарном хекс и десятичном значении и видишь всю динамику такой какой она есть. Да и поднять уарт в разы быстрее чем довести до ума отладчик и тд. Uart это логи! Это удобные текстовые команды устройству в реалтайме без перекомпиляции (для FPGA это часы и сутки!), это не только экономия времени и нервов а ещё и видиние реальной работы устройства не прерывая и не останавливая переферию дебаггером!
          Не смотрите через замочную скважину светодиода на мир, откройте чёртову дверь UARTa!
          Не забывайте про уарт, uart хороший, uart ваш друг!

          Добавлено: Sdima1357 Надеюсь моя uart «доктрина» заслуживает STM32 больше чем мигание светодиодом?
            0

            Есть классы для uart, spi, can. К примеру, для urat сделан modbus rtu, wake.

              0
              Мой отдельный плюсик за CAN, буду очень рад
              0
              «добавлено для sdima»
              Ну наверное. Я например на нем Синклер — Спектрум эмулятор habr.com/ru/post/412325 писал для начала.
              То есть светодиод ту меня был конечно, но я решил не делиться таким результатом.
              Тут много неплохих статей на про stm на хабре
            +1

            Отличная статья, продолжай в том же духе!

              0
              Ок. Буду готовить следующую.
                +1
                Только просьба — проверяйте правописание, «оно хорошее, но почему-то хромает».
              +8
              Не совсем вас понял.
              В начале вы пишите:
              Когда я попытался сделать первый проект меня ждало разочарование — CMSIS! Кому как, но для меня это было (и остается) ужасом: много буков, длинные и для меня не понятные структуры. Вникать во все это было не интересно.

              А ближе к концу углубляетесь еще «ниже» и пишете свои структуры, причем оперируйте адресами регистров, которые в CMSIS уже прописаны.
              В ваши структуры тоже нужно будет другим вникать.
              Смысл?
                0

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

                  +1
                  Отвечу за автора — CMSIS для разраба, избалованного Python'ом или JS выглядит как шестиглавая медуза, у которой при каждом побежденном методе отрастает десять регистров, пять адресов в памяти со смещением и volatile. И, главное — ничерта не понятно, зачем оно все нужно и как работает.
                  +4
                  Не знаю, в каком году ты начинал с ним работать, но уже несколько лет есть генератор проектов CubeMX и таким велосипедостроением заниматься уже просто неприлично.
                    +3
                    В целом я с Вами согласен, но иногда он (cube) генерит такую индусскую муть, что просто страшно…
                      –1
                      А зачем на нее смотреть, если все работает как нужно?
                        +2
                        Не всегда, не всегда. Это пока светодиодик, то оно конечно отлично, а вот что посложнее…
                        Навскидку USB host,Ethernet,sd card, не работают как положено, и полно багов.
                        Вот тогда и приходится ковыряться
                          +1
                          Когда не работает, ковыряться придется в любом случае.
                          Только в первом случае, придется ковыряться в исходниках компании разработчика, имея возможность найти решение проблемы в интернете. Если с ним кто-то уже сталкивался.

                          И совсем другое дело, когда проблемы возникают с посторонней библиотекой, которую кроме автора мало кто использует.
                            +1
                            Это да конечно, но похоже что на софте там пару человек и те из Индии.
                            Железячники у них отличные, а программисты так себе или просто очень сильно перегружены
                            0
                            Ну пока светодиодик, можно и ручками писать, а вот когда нужно cделать что то приличное, вот смотрите как это делали мы в Embox
                              0
                              Ну да вполне приличная ОС и статьи интересные
                              +1
                              У ST есть рабочие примеры подо всю периферию. Там смотри.

                              www.st.com/en/embedded-software/stsw-stm32068.html

                              Семейство нужное выбирай и качай.
                                +1
                                Это не так. Оно там далеко не все работает.там часто код например под IAR работает а под GCC нет. Например забудут сегмент cache free определить, и ты неделю другую не понимаешь почему иногда приходят испорченные пакеты в Ethernet. это как правило незаметно, но иногда полсекунды задержка в передаче
                            +2
                            Работает? Обновляется? Переносится на другие контроллеры семейства? Сколько самодельным библиотекам до этого?
                            +2

                            Поддержу, Sdima1357 Cube генерит столько кода… чтобы поморгать светодиодом. Это универсальная библиотека для быстрого прототипирования, она не годится для нормальных продуктов, связанных с надежностью.
                            Быстро что-то проверить -самое то, но писать продуктовый код на ней не лучшее решение (ИМХО).

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

                              Пример «нормальных продуктов» в студию. Обычно такие аргументы у диванных теоретиков.
                                +1

                                Да любой промышленныйи датчик. Ну вот, например, https://www.emerson.ru/ru-ru/catalog/metran-150-ru-ru
                                Вы ни одного сертификата безопасности не получите с авто-сгенерировангой ерундой из Cube. Либо придется всю эту лапшу проверять юнит тестами… Вы как себе это представляете?

                              0

                              А кубик поддерживает миландровские контроллеры?

                                0
                                Их даже миландр нормально не поддерживает. У них должна отличаться периферия и поэтому драйверы не будут совместимы.
                              +2
                              Ну вот опять, кто-то пытается победить CMSIS новым методом. А метод оказывается старым, уже не актуальным на данный момент.
                              Оборачивание регистров в битовые структуры — не уменьшает количество Си кода, и значительно увеличивает размер бинарного кода. Метод от самой ST — тоже болеет этой фигнёй. Идеального решения не существует, и никакие библиотеки с новыми методами тут не помогут. Если в регистре есть 32 отдельных поля для записи — то все 32 поля придётся описать. Описание должно быть в одном месте, без размазывания по всему проекту. Хотя данное требование не относится ко всему что есть в мк.

                              Откройте для себя макросы _VAL2FLD и _FLD2VAL, это позволит частично сократить чтение документации, и значительно сократить время поиска ошибок. Основной бонус этих макросов — отсутствие дополнительного слоя абстракции, CMSIS используется как есть — в собственном соку.
                                +2
                                Очень хорошая статья на тему, как делать не нужно. На самом деле очень даже опасная — подобные статьи читают в основном новички, начинают делать также, а потом их приходится бить по рукам.

                                1) Вообще, после фразы «Когда я попытался сделать первый проект меня ждало разочарование — CMSIS! Кому как, но для меня это было (и остается) ужасом: много буков, длинные и для меня не понятные структуры. » программиста можно выгонять с собеседования, так как это явно не эмбеддер, язык С он точно не знает, как следствие и С++ тоже.
                                Ну да:
                                RCC_APB2ENR_bit.ADC1EN = 1; 
                                RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;


                                Эти записи такие разные, явно стоит вендорлока, ещё и свой аналог для описания регистров нагородим. Вы же его автоматически генерируете? И тесты у вас есть?

                                2) IAR — компилятор для профессионалов(нет). Только в заголовке будет запись типа:
                                #error This file should only be compiled by ARM IAR compiler and assembler
                                IAR платный, а вы не оставляете мне возможности собрать проект другим компилятором. Может быть мне его купить за 2-3к$? А у вас он куплен?
                                Под линукс он не поставляется, наверное, придется ещё и винду покупать. Ну да ладно: «Я привык работать в IAR. Да, есть другие IDE, но мне хватает возможности IAR».

                                3) Как правило, начинающие разработчики изучают С++ со стороны ООП и это вполне себе оправданно, но это не серебряная пуля. C++ — мультипарадигмальный язык программирования, наиболее мощными возможностями которого являются метапрограммирование и программирование в пространстве типов, позволяющие сгенерировать оптимальный код, который так важен в эмбеддед.
                                Все расчеты в коде, который вы привели, можно выполнить на этапе компиляции, сведя все к записям в регистры GPIO, у вас это все считается в рантайме. А если у вас по ТЗ время перехода МК в некое состояние 1мс, при этом настроить нужно 200 ног?
                                Посчитать на этапе компиляции маски можно вот так

                                Использование будет примерно таким:
                                struct LedTrait final: mpp::gpio::LedTrait
                                {
                                    constexpr static mpp::gpio::Inversion kInversion = mpp::gpio::Inversion::Off;
                                };
                                //...
                                using LedBlue   = mpp::gpio::Gpio < mpp::gpio::PD15, LedTrait >;
                                //...
                                using Leds = mpp::gpio::IoGroup < LedBlue, LedRed, LedOrange, LedGreen >;


                                А помигать можно как-то так :
                                Leds::Toggle();


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

                                Возможно, вам стоит изучить язык более досконально, С++20 рановато брать, а вот С++17 давно мейнстрим. Уже потом учить работников и писать статьи.
                                  –1
                                  Очень хорошая статья на тему, как делать не нужно

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

                                  Не повезло Вашим подчиненными, если таковые имеются.
                                  программиста можно выгонять с собеседования, так как это явно не эмбеддер, язык С он точно не знает, как следствие и С++ тоже.

                                  да, хорошо чувствовать превосходство. Только когда появляется на собеседовании человек, который покажет, что Вы плохо знаете, то его тоже отсеете?
                                  Эти записи такие разные, явно стоит вендорлока, ещё и свой аналог для описания регистров нагородим. Вы же его автоматически генерируете? И тесты у вас есть?

                                  Вы о чем?
                                  и да, CMSIS тестируют на пользователях, не просто так вссплывают опечатки и баги.
                                  2) IAR — компилятор для профессионалов(нет). Только в заголовке будет запись типа:
                                  #error This file should only be compiled by ARM IAR compiler and assembler
                                  IAR платный, а вы не оставляете мне возможности собрать проект другим компилятором. Может быть мне его купить за 2-3к$? А у вас он куплен?
                                  Под линукс он не поставляется, наверное, придется ещё и винду покупать. Ну да ладно: «Я привык работать в IAR. Да, есть другие IDE, но мне хватает возможности IAR».

                                  где Вы в коде нашли не совместимость с другими компиляторами? я порекомендовал IAR, т.к. мне он показался удобней. но это не означает, что все поголовно должны перейти на него.
                                  у меня iar не куплен, лицензия (как написано в тексте) ограничение кода. для дома и семьи хватает.
                                  За лицензию платит заказчик, когда я отдаю исходники. И вообще, почему хороший инструмент должен быть бесплатным?
                                  3) Как правило, начинающие разработчики изучают С++ со стороны ООП и это вполне себе оправданно, но это не серебряная пуля. C++ — мультипарадигмальный язык программирования, наиболее мощными возможностями которого являются метапрограммирование и программирование в пространстве типов, позволяющие сгенерировать оптимальный код, который так важен в эмбеддед.

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

                                  можно. каждый выбирает свой путь
                                  А если у вас по ТЗ время перехода МК в некое состояние 1мс, при этом настроить нужно 200 ног?

                                  не попадалось. не уверен, что на все случаи жизни можно сделать что-то универсальное.
                                  А помигать можно как-то так :

                                  посмотрел Ваш стиль написания. Красиво. Молодец.
                                  void board::Init()
                                  {
                                    <b>RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;</b>
                                  	
                                    board::Systick::Init();
                                    board::ClockCounter::Init();
                                    board::Leds::Init();
                                  	
                                    return;
                                  }

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

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

                                  буду стараться
                                  Уже потом учить работников и писать статьи.

                                  а кто будет учить работников? ждать Вас? мне нужны специалисты сейчас.
                                  А вот от Вас ждем интересный статей. Я серьезно, поделитесь опытом.
                                    +1
                                    Под линукс он не поставляется, наверное, придется ещё и винду покупать

                                    Уже вот вот будет… :) Вроде даже уже есть, но пока мне еще не дали.


                                    Посчитать на этапе компиляции маски можно вот так

                                    Там у вас тоже странности в коде есть


                                    inline constexpr static void Init() noexcept(true) {
                                              GPIO_TypeDef* regs { reinterpret_cast<GPIO_TypeDef*>(kPort) };

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

                                    –2

                                    Открываю статью, вижу IAR — закрываю статью.


                                    Почему? Потому что IAR и Keil — пропиаритарны.
                                    Многие возразят: есть же free, с ограничением по размеру кода… или условиям использования…
                                    Отвечаю: есть, но я не хочу приучаться к системе, которая в случае разрастания кода или не дай бог комерциализации моего проекта может потребовать несколько тысяч долларов — сразу.


                                    Я много раз пытался завести STM на Qt+GCC ARM под винды, но ниразу у меня не вышло. (под линух смысла нет, т. к. основная ОС — для моих любительских и учебных проектов — виндовс) Счас мне напихают — Qt тоже пропиоритарен. Отвечу на сколько последний раз сталкивался: Community подразумевает требование публикации исходных кодов даже для коммерческих продуктов. Меня такой подход — на данном этапе возможной комерциализации моих проектов — устраивает.


                                    Последний раз я дрюкался с какой то IDE от самих STM… Вроде работает… Но в STM так и не вступил, и не только из-за геморности с IDE, но и отсутствием подходящего проекта… Простите, но моргать диодиком я уже на AVRкак в доврдуиновскую эпоху наморгался.


                                    Кто нибудь! Напишите нормальную статью именно по старту, для тупых магистров/аспирантов. А то не дай бог, честное слово! самому прийдется начинать писать!

                                      +1
                                      Открываю статью, вижу IAR — закрываю статью...

                                      захожу в магазин, вижу Windows на ноутбуке — выхожу из магазина…
                                      Если Вы учитесь — производители предлагают бесплатно попользоваться, если Вы зарабатыаете деньги, почему другие не должны это делать.
                                      Если Вы обучаете студентов — обратитесь к разработчикам, возможно, есть лицензия для ВУЗов.
                                      А то что бесплатно — придется вручную прикручивать. Тут ничего не поделать.
                                        0

                                        IAR использует Clang, поэтому вполне себе нормальный он. Основное преимущество, что поддержка есть со стороны производителя и сертификаты и это дефакто стандарт для промышленных компаний (по крайней мере на западе). У него есть 30 кБайтная версия для обучения.
                                        Вообще без разницы на чем делать, код написанный на С++17 должен на любом компиляторе собираться. Там есть тонкости с прагмами и заданием сегментов и встроенных функций, но они минимальны. Все отличия можно в одном файле держать.
                                        Поэтому насчет компиляторов, тут без разницы — для встроенщиков осталось всего два вида Clang и GCC и работать такой код должен везде. А IDE можно любую к ним прикрутить....

                                          0

                                          В том то и проблема с "быстрым стартом", либо IDE в комплекте с компилятором — платная, либо нужно с бубном попрыгать, что-бы "helloworld" запустить.
                                          И это в случае с STM/ARM. С AVRовским семейством от микрочипа — таких проблем нет.
                                          Я уж не говорю про миландровские "клоны STM f103" — эти только на IARe у нас в конторе смогли запустить.

                                            0

                                            У них библиотека для дебагера отличается. Вроде, для keil тоже есть, не помню.
                                            Делал на миландр проект, как раз в iar. Жаль, что для stlink ничего не сделали, только j-link. Сначала чип понравился, заработало почти все и сразу. Но потом повылезали нюансы, связанные с особенностью чипа. Errata достаточно объёмная (хорошо или плохо), не все проблемы можно решить. С can там танцы с бубном. Недавно коллеги по несчастью выяснели — частота уходит, будем на неделе изучать феномен.

                                            0

                                            Самое интересное, что 30 КБ это ограничение на код. На константы, хранимые во flash (например, картинки) я не заметил.

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

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