Забыл предупредить. Всё, что я рассказываю, справедливо практически для всех ядер МК. Я привожу примеры для 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_ */
В нашем классе два конструктора идут друг за другом:
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 на выход, без подтяжки.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.
Тема на форуме - перейти.
Класс! Классно!