Русский
Русский
English
Статистика
Реклама

Очередная статья 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, но это не интересно.
Спасибо за уделенное время. Если интересно продолжение дайте знать.
Источник: habr.com
К списку статей
Опубликовано: 12.09.2020 20:07:24
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование микроконтроллеров

Stm32

Iar

C++

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru