Top.Mail.Ru

Часть 6. STM32 и С++. GPIO на классах.

Забыл предупредить. Всё, что я рассказываю, справедливо практически для всех ядер МК. Я привожу примеры для STM32F4xx. Но в архивах и на Яндекс Диске есть всё то же самое для МК STM32F0xx, STM32F1xx, STM32F4xx, STM32L0xx, STM32L1xx. Всё это богатство в разной степени готовности. Так как писалось это всё в разное время, библиотеки могут или отличаться от "канона", или могут быть не дописаны. Я привожу в качестве примера STM32F4xx, так как чаще с ним работал и сейчас делаю на STM32F407VET6 довольно крупный проект, поэтому библиотеки к нему находятся в более-менее вменяемом состоянии. Если в библиотеках на другие МК чего-то не хватает, по статьям и примерам можно повторить эти шаги, дописать или поправить самим, а я потом добавлю. Таким образом добьёмся того, чтобы наш труд не пропал даром.

А теперь продолжим. На каталоге Library жмем правой кнопкой мыши и выбираем создание класса:

Обзываем класс IO_Digital, оставляем только конструктор, правим немного имена файлов, так как IDE в них не добавила подчёркивание, и давим ОК:

Создаются два файла, IO_Digital.h и IO_Digital.cpp, но не в тех каталогах, что нам нужны. Что-то ST эту фичу не доработала или я не понял, как ей пользоваться:

Теперь можно мышкой просто растащить их по нужным каталогам. IO_Digital.h в Library\Inc, а IO_Digital.cpp в Library\Src:

Файлы изначально откроются в окне редактора и будут иметь такой вид, IO_Digital.h:

/*
 * IO_Digital.h
 *
 *  Created on: 10 февр. 2022 г.
 *      Author: User
 */

#ifndef IO_DIGITAL_H_
#define IO_DIGITAL_H_

class IO_Digital
{
  public:
    IO_Digital();
};

#endif
 /* IO_DIGITAL_H_ */

IO_Digital.cpp:

/*
 * IO_Digital.cpp
 *
 *  Created on: 10 февр. 2022 г.
 *      Author: User
 */
#include "IO_Digital.h"
IO_Digital::IO_Digital()
{
  // TODO Auto-generated constructor stub
}

 

Теперь нам надо написать в IO_Digital.h функции, которые должен выполнять наш класс. Их немного. Так как не было пока потребности в других. Также не забываем включить в файл #include"STM32.h", так как у нас там ссылки на программное ядро.

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

/*
 * IO_Digital.h
 *
 *  Created on: 10 февр. 2022 г.
 *      Author: User
 */

#ifndef IO_DIGITAL_H_
#define IO_DIGITAL_H_

#include "STM32.h"

class IO_Digital
{
  public:
    IO_Digital(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, bool IO, uint8_t PULL);              // Инициализация порта
    IO_Digital(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, bool IO, uint8_t PULL, bool Edge);   // Инициализация порта с предустановкой его в
                                                                                            // одно из состояний
    void digitalWrite(bool DWrite);                                                         // Вывод на пин 0 или 1
    bool digitalRead(void);                                                                 // Чтение состояния пина
    void digitalTogglePin(void);                                                            // Переключение пина в противоположное состояние

  private:
    GPIO_TypeDef  *_GPIOx;                                                                  // Переменная, содержащая имя порта
    uint16_t      _GPIO_Pin;                                                                // Переменная, содержащая позицию вывода
    bool          _IO;                                                                      // Переменная хранит тип - вход/выход.
    uint8_t       _PULL;                                                                    // Переменная хранит тип подтяжки VCC/GND/NONE
};


#endif /* IO_DIGITAL_H_ */

В нашем классе два конструктора идут друг за другом:

  1. IO_Digital(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, bool IO, uint8_t PULL). Он инициализирует GPIOx - порт, GPIO_Pin - вывод/выводы порта, IO - настройка порта на вход или на выход, PULL - подтяжка порта. Например: IO_Digital Led(GPIOС, GPIO_PIN_0, Output, No_Pull). Мы инициализируем вывод PC0 на выход, без подтяжки.
  2. IO_Digital(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, bool IO, uint8_t PULL, bool Edge). То же самое, только добавлена переменная Edge, которая указывает в каком состоянии должен находится вывод после инициализации "0" или "1".

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

Далее идут три функции, назначение которых описано в комментариях. Всё это счастье должно находится в секции public, чтобы наша программа могла их видеть. В секции private описаны переменные, которые вызывающей программе недоступны, и используются классом для личных нужд.

Далее в IO_Digital.cpp описываем, что каждая функция должна делать:

IO_Digital::IO_Digital(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, bool IO, uint8_t PULL)
{
  _GPIOx = GPIOx;                                                   // Запоминаем номер порта
  _GPIO_Pin = GPIO_Pin;                                             // Номер вывода
  _IO = IO;                                                         // Тип вывода - вход/выход и т.д
  _PULL = PULL;                                                     // Подтяжка
  _SetPin(_GPIOx, _GPIO_Pin, _IO, _PULL);                           // Вызываем инициализацию
}

IO_Digital::IO_Digital(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, bool IO, uint8_t PULL, bool Edge)
{
  _SetEdge(Edge);                                                   // Говорим, какой уровень нам нужен после инициализации
  IO_Digital(GPIOx, GPIO_Pin, IO, PULL);                            // Вызываем основной конструктор
  _SetEdge(Low);                                                    // Сбрасываем переменную к значению по умолчанию
}

void IO_Digital::digitalWrite(bool DWrite)
{
  _DigitalWriteBit(_GPIOx, _GPIO_Pin, DWrite);                      // Вызываем нашу старую знакомую функцию записи бита
}

bool IO_Digital::digitalRead()
{
  return _DigitalReadBit(_GPIOx, _GPIO_Pin);                        // Вызываем функцию чтения бита
}

void IO_Digital::digitalTogglePin(void)
{
  if(_GPIOx->IDR & _GPIO_Pin)                                       // Проверяем, в каком состоянии вывод на данный момент
    _GPIOx->BSRR = (_GPIO_Pin << 16);                               // Если был 1, сбрасываем в 0
  else
    _GPIOx->BSRR = _GPIO_Pin;                                       // Если был 0, устанавливаем в 1
}

Эти функции очень просты и нет смысла описывать их досконально. Если посмотреть на содержимое, будет видно, что мы вызываем уже ранее написанные в gpio_main.cpp функции. Возникает вопрос, а зачем мы городили этот огород?

Если посмотреть пример моргания диодом из предыдущей статьи, он выглядит так:

while (1)
{
  _DigitalWriteBit(GPIOC, GPIO_PIN_0, High);  // Переводим вывод в высокий уровень
  delay(500);                                 // Задержка полсекунды
  _DigitalWriteBit(GPIOC, GPIO_PIN_0, Low);   // Переводим вывод в низкий уровень
  delay(500);
}

Как видим, для того, чтобы переключить вывод из одного состояния в другое, нам каждый раз нужно указывать порт и вывод. Многих это устраивает. Но мне захотелось чтобы всё было "по-взрослому". И теперь инициализация необходимого нам вывода и работа с ним будет выглядеть следующим образом...

Перед функцией main() объявляем объект класса, который будет управлять светодиодом: IO_Digital Led(LED_GPIO, LED_PIN, Output, No_Pull). В самой функции main() в бесконечном цикле моргаем светодиодом следующим образом:

while(1)
{
  Led.digitalWrite(High);                       // Устанавливаем выход в высокое состояние
  delay(500);
  Led.digitalWrite(Low);                        // Устанавливаем выход в низкое состояние
  delay(500);
  // Можно ещё так
  // Устанавливаем выход в противоположное состояние. Так как предыдущая команда установила его в "0",
  // эта команда переведёт его в противоположное состояние, т.е. "1"
  Led.digitalTogglePin();
  delay(200);
  // Устанавливаем выход в противоположное состояние. Так как предыдущая команда установила его в "1",
  // эта команда переведёт его в противоположное состояние, т.е. "0"
  Led.digitalTogglePin();
  delay(200);
}

Как видите, здесь мы не используем ни имя порта, ни номер вывода. Всё за нас помнит объект, который мы обозвали Led.

Примерно так будет выглядеть наше дальнейшее программирование. Только есть небольшая проблема. На несложную периферию классы пишутся довольно просто. Только со сложной возникнут проблемы. Взять к примеру таймер. Запустить его под классами проблематично, так как у него очень много режимов. Описать каждый режим и их взаимодействие друг с другом будет довольно сложно. Поэтому на первое время в виде классов можно будет описать только базовые функции. А если сюда ещё и добавить DMA, тогда я вообще пока не представляю, возможно ли это.

На этом пока всё. Особые благодарности моим учителям и вдохновителям:

Пример из статьи находится в каталоге WorkDevel\Developer\Tests\MK\F407\F407VExx_GPIO_Class, ссылка на Яндекс Диск, всё одним архивом WorkDevel.

Подписаться
Уведомление о
guest
1 Комментарий
старее
новее
Inline Feedbacks
View all comments
Aleksey_Baydin
Aleksey_Baydin
3 месяцев назад

Класс! Классно!

1
0
Would love your thoughts, please comment.x
()
x