Очередная статья: 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, но это не интересно.
Спасибо за уделенное время. Если интересно продолжение — дайте знать.

ссылка на оригинал статьи https://habr.com/ru/post/518832/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *