Данная статья будет последней вводной статьей перед тем, как мы начнём писать классы!
GPIO. Общая концепция инициализации.
Вверху на схеме - базовая структура порта. Может помочь в понимании дальнейшего текста. Характерна для Cortex-M0. Для других может незначительно отличаться. Список регистров для разных ядер также может отличаться, особенно эта разница заметна у Cortex-M3. С ними разберёмся позже, сейчас же рассмотрим общие вопросы программирования портов. Основные шаги инициализации:
- Включить тактирование порта.
- Установить скорость порта.
- Установить тип порта.
- Установить , куда подтянут вывод - к VCC, к GND или висит в воздухе.
- Указать, чем порт у нас будет заниматься.
- Если порт подключен к какому либо устройству, задать альтернативную функцию.
Теперь разберём данные шаги.
Шаг 1. Включение тактирования.
Без этого дальнейшие действия бесполезны. Тактирование включается в регистрах тактового генератора. Причём регистр, в котором это делается, в разных контроллерах может быть разный для конкретного порта, чаще всего это регистр AHB*ENR. Звёздочки могут быть заменены на номер регистра, если периферии много, то и регистров таких бывает несколько. Для серии Cortex-M0 - это регистр AHBENR. Для других ядер МК нужно смотреть документацию, так как этот регистр может оказаться другим.
Выглядит он следующим образом:
Как видно из рисунка, здесь можно включить тактирование для портов GPIOF, GPIOE, GPIOD, GPIOC, GPIOB, GPIOA. Перечислены все порты, которые могут встретиться в этой серии, но наличие и количество портов в конкретном кристалле нужно смотреть или в даташите или в CubeMX. И ещё нужно не забывать, если порт не используется вообще, его тактирование лучше не включать - это увеличит энергопотребление.
Тактирование включаем следующей операцией RCC->AHBENR |= RCC_AHBENR_GPIOAEN
.
Шаг 2. Скорость порта.
Данный регистр отвечает за скорость переключения порта (по крайней мере я так понял). Порт может переключаться с разной скоростью, задаваемой в регистре OSPEEDR. Переключение происходит не моментально, а за несколько тактов. Чем выше скорость, тем меньше тактов необходимо для переключения. Сделано это для снижения энергопотребления и управления медленными устройствами. Точнее об этом можно узнать, прочитав даташит. В этом регистре также отводится два бита на вывод:
- х0 — низкая скорость
- 01 — средняя скорость
- 11 — высокая скорость
Инициализируются они так же, как и в регистре MODER.
Шаг 3. Тип порта.
Значение данного регистра имеет смысл только тогда, когда вывод настроен на выход и принимает значения:
- 0 — обычный выход который можно перевести в 0 или 1 (push-pull).
- 1 — открытый исток.
На выходе имеются два транзистора - в верхнем и нижнем плече. Когда на выходе 1, открыт верхний транзистор, а нижний закрыт. При 0 - наоборот. При открытом истоке верхний транзистор как бы исключается из схемы (всегда в закрытом состоянии), и исток нижнего транзистора оказывается зависшим в воздухе. Этот тип включения используется для соединения по схеме "монтажное ИЛИ". Таким образом можно объединить несколько выходов разных устройств, подтянув их одним резистором к питанию. Таким способом управляется шина I2C.
За данный режим у нас отвечает регистр OTYPER:
Для его инициализации используется команда: GPIOA->OTYPER |= 1 << I. Для сброса: GPIOA->OTYPER &= ~(1<<I)
.
Шаг 4. Подтяжка.
Данный регистр отвечает за подтяжку вывода к питанию или к земле. Режим задаётся в регистре PUPDR, он имеет также по два бита на вывод:
- 00 — подтяжки нет
- 01 — подтяжка к питанию
- 10 — подтяжка к земле
- 11 — зарезервировано (не используется)
Этот регистр можно использовать, когда необходимо подключать кнопки, программный I2C и многое другое, где необходима подтяжка куда-либо, вверх или вниз.
Теперь ложка дёгтя. На эту подтяжку лучше никогда не надеяться, хоть и можно включать и использовать в несложных помехонезащищённых устройствах. Почему так? Я по крайней мере сталкивался с 3-мя случаями, когда внутрення подтяжка меня подводила:
- Подтяжка осуществляется только тогда, когда вывод включен. Если вывод в Z-состоянии, подтяжка отключается. А получить Z-состояние весьма просто - включить таймер на ШИМ, а потом, когда нужно ШИМ отключить, не отключать сам таймер, а просто запретить ему вывод в порт. И вывод оказывается в Z-состоянии. Силовые ключи, которые не имели собственной подтяжки по входу, оказывались никуда не подключенными, и малейшая наводка на входы открывала их. В результате из контроллера для трёхфазного двигателя вышел «священный дым» с пиротехническими эффектами. А без «священного дыма» никакая электроника работать не может.
- Случай энергосбережения. В некоторых режимах также используется перевод выходов в Z-состояние, и внешняя периферия оказывается в подвешенном состоянии, и в результате вместо снижения потребления получался обратный эффект, когда периферия начинала идти вразнос, и потребление увеличивалось.
- Случай недостаточности тока. Сопротивление внутренних подтягивающих резисторов очень велико, порядка 50 КОм и выше, что даёт очень маленькие нагрузочные токи. Таким образом получается, что тока, который протекает через этот резистор, может оказаться недостаточно для срабатывания какой-либо периферии. Это полбеды. Плохо, когда она балансирует на грани. И любая самая малая помеха на выводе может или привести устройство в негодность, или вызвать ложное срабатывание, или контроллер среагирует неадекватно, приняв помеху за действительный сигнал (ага, блок управления атомным реактором). Не зря ведь советуют для I2C устройств использовать подтяжку 4.7 КОм.
Шаг 5. Указываем, чем у нас порт будет заниматься.
Он может работать:
- 00 - на вход (значение по умолчанию при сбросе)
- 01 - на выход
- 10 - альтернативная функция - когда вывод подключен к внутреннему устройству, например, ЦАП, АЦП, вход/выход таймера и так далее...
- 11 - аналоговый режим. Хоть не все входы могут быть подключены к АЦП, но перевод в аналоговый режим отключает входной триггер Шмитта, таким образом снижая энергопотребление чипа.
За всё это счастье у нас отвечает регистр MODER:
Чтобы проинициализировать его достаточно пары команд:
GPIOA->MODER &= ~(0b11 << (I * 2)); // Стираем биты, соответствующие данному порту GPIOA->MODER |= 0b01 << (I * 2); // Устанавливаем порт на выход
Почему команд пара? Можно обойтись только второй. Но в случае, если инициализация порта повторная после сброса, может оказаться так, что предыдущая инициализация переключила порт в альтернативный режим, а попытка перевести порт в режим выхода переключит его в режим аналогового входа, так как старший бит окажется установленным. Поэтому лучше на будущее взять в привычку сбрасывать биты перед инициализацией.
В данной команде присутствует переменная I. Это просто номер вывода, который нужно запрограммировать. Этот номер умножается на 2, так как для каждого вывода используются два бита, поэтому сдвиг нам нужен двойной, чтобы записать функцию вывода в пару ячеек, которая заведует нужным выводом. Можно конечно просто прописать цифрами нужные значения, но данный способ необходим для написания универсальной функции инициализации. Что и будет показано ниже.
Ну и самое страшное.
Шаг 6. Альтернативная функция.
Вывод подключается «альтернативно» к какому-либо внутреннему узлу - UART, I2C, TIM и так далее. В данном случае вывод будет запрограммирован или на вход или на выход согласно своей выполняемой функции. Поэтому это уже не наша забота, как его программировать, за исключением схем с открытым истоком (типа I2C), в этом случае нужно перевести выход в Open Drain. Список альтернативных функций приведён в даташите, приводить здесь их нет смысла, больно они здоровые.
И есть ещё маленькая хитрость. Мне лениво самому писать дефайны описания альтернативных функций, поэтому можно в HAL позаимствовать файл stm32f0xx_hal_gpio_ex.h (для других кристаллов есть соответствующие), обозвать его по-своему и использовать в своих проектах. Обычно я его обзываю stm32f0xx_gpio.h и копирую в каталог STM32Lib\Device\Stm32F4xx\Inc.
Для программирования альтернативных функций имеются два регистра: AFRL и AFRH. Так как на каждый вывод используется 4 бита, пришлось их разделять на старший и младший. Младший конфигурирует выводы 0-7 порта, старший 8-15:
Таким образом, мы получаем следующий код инициализации. Например, для порта GPIOA и пина PA1, для обычной инициализации вывода на выход:
// Шаг 1. Включение тактирования RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // Шаг 2. Скорость порта GPIOA->OSPEEDR &= ~(0b11 << (1 * 2)); // Сбрасываем значение регистра GPIOA->OSPEEDR |= 0b11 << (1 * 2); // Самая высокая скорость // Шаг 3. Тип порта GPIOA->OTYPER &= ~(1U << 1); // Стирая бит устанавливаем тип вывода push-pull // Шаг 4. Подтяжка GPIOA->PUPDR &= ~(0b11 << (1 * 2)); // Сбрасываем значение регистра GPIOA->PUPDR |= (0b01 << (1 * 2)); // Устанавливаем подтяжку к питанию // Шаг 5. Указываем что вывод запрограммирован на выход GPIOA->MODER &= ~(0b11 << (1 * 2)); // Сбрасываем порт GPIOA->MODER |= (0b01 << (1 * 2)); // Устанавливаем его на выход
Альтернативную функцию будем рассматривать при инициализации периферии.
А сейчас быстренько пробежимся по функциям. Они находятся в каталоге STM32Lib\Device\Stm32F4xx, файлы gpio_main.h и gpio_main.cpp. Нужно не забыть прописать #include "stm32f4xx_gpio.h"
и #include "gpio_main.h"
в STM32.h, так как они будут глобальными и будут использоваться во всех классах, которые мы будем писать в дальнейшем:
// Функции инициализации пинов void _SetPin(GPIO_TypeDef *GPIOx_Set, uint16_t GPIO_Pin_Set, uint8_t IO_Set, uint8_t PULL_Set); // Инициализация одного или нескольких пинов void _SetPinAlternate(GPIO_TypeDef *GPIOx_Set, uint16_t GPIO_Pin_Set, uint8_t Alternate_Set); // Инициализация альтернативной функции одного или нескольких пинов // Функции изменения переменных инициализации void _SetSpeed(uint8_t Speed_Set); // Установка скорости порта void _SetPull(uint8_t Pull_Set); // Установка подтяжки void _SetTypeOut(uint8_t TypeOutput); // Тип выхода // Работа с битами bool _DigitalReadBit(GPIO_TypeDef *GPIOx_Set, uint16_t GPIO_Pin_Set); // Чтение бита void _DigitalWriteBit(GPIO_TypeDef *GPIOx_Set, uint16_t GPIO_Pin_Set, bool Level); // Запись бита void _DigitalWrite(GPIO_TypeDef *GPIOx_Set, uint16_t Data); // Запись 16-разрядных данных
Основных функций инициализации портов всего две. Обычная инициализация:
void _SetPin(GPIO_TypeDef *GPIOx_Set, uint16_t GPIO_Pin_Set, uint8_t IO_Set, uint8_t PULL_Set);
Эта функция инициализирует указанный в переменной GPIOx_Set
порт, в GPIO_Pin_Set
указан пин или группа пинов, в IO_Set
указываем, что это у нас - вход/выход или что-то ещё, в PULL_Set
- куда у нас сделана подтяжка. По умолчанию скорость порта низкая, выход настраивается как Push-Pull. Пример для GPIOA, PA1:
_SetPin(GPIOA, GPIO_PIN_1, Output, Pull_Up);
Пример для GPIOA и PA1, PA2, PA8:
_SetPin(GPIOA, GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_8, Output, Pull_Up);
Инициализация альтернативной функции:
void _SetPinAlternate(GPIO_TypeDef *GPIOx_Set, uint16_t GPIO_Pin_Set, uint8_t Alternate_Set);
Аргументы тут практически аналогичны предыдущей функции. Пример инициализации альтернативной функции выводов для работы с USART1 - порт GPIOA и выводы PA9, PA10:
_SetPinAlternate(GPIOA, GPIO_PIN_9 | GPIO_PIN_10, GPIO_AF7_USART1);
Какие альтернативные функции, и для чего служат, можно посмотреть в файле stm32f0xx_gpio.h или в даташите.
Иногда в альтернативной функции нужна другая скорость порта, режим Open-Drain или установка подтяжки не по умолчанию. Для этого можно вызвать нужную функцию из списка:
// Функции изменения переменных инициализации void _SetSpeed(uint8_t Speed_Set); // Установка скорости порта void _SetPull(uint8_t Pull_Set); // Установка подтяжки void _SetTypeOut(uint8_t TypeOutput); // Тип выхода
Внимание! Они должны вызываться ДО вызова функций _SetPin()
и _SetPinAlternate()
, так как они изменяют только переменную инициализации, которая у нас стоит по умолчанию. А после отработки функции инициализации порта желательно вернуть значения в исходное состояние, иначе функции инициализации в других объектах будут пользоваться другими установками, что в целом не страшно, кроме одного режима - Open-Drain.
Ну и оставшиеся две функции просто ставят нужный пин в нужном порту в 0 или 1. Или читают с нужного порта, что там у него прилетело. Быстренько пробежимся по коду. Мы объявляем три статические переменные, которые не должны уничтожаться и должны сохранять свои значения при выходе из функции. Их мы меняем с помощью функций работы с переменными инициализации:
static uint8_t _GPIO_Speed = Speed_Low; static uint8_t _GPIO_Pull = No_Pull; static uint8_t _TypeOut = Out_Normal;
И рассмотрим функцию _SetPinAlternate()
как наиболее интересную, функция _SetPin()
практически ей идентична:
void _SetPinAlternate(GPIO_TypeDef *GPIOx_Set, uint16_t GPIO_Pin_Set, uint8_t Alternate_Set) { // !!! У Cortex-M4 AFIOEN в RCC нет. Для других контроллеров нужно быть внимательнее GPIO_TypeDef *_GPIOx_Set = GPIOx_Set; uint16_t _GPIO_Pin_Set = GPIO_Pin_Set; if(_GPIOx_Set == GPIOA) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; else if(_GPIOx_Set == GPIOB) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; else if(_GPIOx_Set == GPIOC) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; else if(_GPIOx_Set == GPIOD) RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; else if(_GPIOx_Set == GPIOE) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN; else if(_GPIOx_Set == GPIOF) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOFEN; else if(_GPIOx_Set == GPIOG) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN; else if(_GPIOx_Set == GPIOH) RCC->AHB1ENR |= RCC_AHB1ENR_GPIOHEN; uint32_t Position; uint32_t IO_Position = 0x00U; uint32_t IO_Current = 0x00U; // Инициализируем вывод for (Position = 0U; Position < 16; Position++) { IO_Position = 0x01U << Position; IO_Current = (uint32_t) (_GPIO_Pin_Set) & IO_Position; if(IO_Current == IO_Position) { _GPIOx_Set->OSPEEDR &= ~(0b11 << (Position * 2)); // Сброс скорости портов _GPIOx_Set->OSPEEDR |= (_GPIO_Speed << (Position * 2)); // Скорость портов _GPIOx_Set->PUPDR &= ~(0b11 << (Position * 2)); // Сброс подтяжек нет _GPIOx_Set->PUPDR |= (_GPIO_Pull << (Position * 2)); _GPIOx_Set->OTYPER &= ~(1U << Position); // Устанавливаем тип вывода push-pull _GPIOx_Set->OTYPER |= (_TypeOut << Position); if(Position < 8) { _GPIOx_Set->AFR[0] &= ~(0b1111 << (Position * 4)); // ARFH стираем только те биты, что будем программировать _GPIOx_Set->AFR[0] |= (Alternate_Set << (Position * 4)); } else { _GPIOx_Set->AFR[1] &= ~(0b1111 << ((Position - 8) * 4)); // ARFL стираем только те биты, что будем программировать _GPIOx_Set->AFR[1] |= (Alternate_Set << ((Position - 8) * 4)); } _GPIOx_Set->MODER &= ~(0b11 << (Position * 2)); // Сброс портов, если там что то есть _GPIOx_Set->MODER |= (0b10 << (Position * 2)); // Альтернативная функция } } }
Как видите, функция полностью повторяет шаги инициализации. Особый интерес здесь представляют две вещи - это цикл и инициализация альтернативной функции. Цикл нужен для того, чтобы сразу инициализировать группу выводов. Если этого не делать, а просто записать "дефайнами" позицию вывода, то пришлось бы каждый вывод инициализировать отдельно. А это был бы громоздкий код, особенно для подключения дисплеев по FSMC.
В файле stm32f0xx_gpio.h выводы описаны таким образом:
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */ #define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */ #define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */ #define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */ #define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */ #define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */ #define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */ #define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */ #define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */ #define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */ #define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */ #define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */ #define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */ #define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */ #define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */ #define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */
Получается, что при инициализации из примера _SetPinAlternate(GPIOA, GPIO_PIN_9 | GPIO_PIN_10, GPIO_AF7_USART1)
- GPIO_Pin_Set = 0x0200 | 0x0400 = 0x0600
. То есть мы имеем два задействованных бита. В этом цикле просто сдвигается единица в переменной IO_Position на количество шагов, равное счётчику Position. Затем делается операция "И" с переменной GPIO_Pin_Set
и, если значение истинно, значит мы нашли номер позиции инициализируемого пина и необходимо произвести инициализацию всех регистров соответствующего вывода.
И вторая особенность: регистров альтернативных функций два - AFRH и AFRL. Так как альтернативные функции кодируются 4 битами, одного регистра хватает только на 8 выводов. Обращение к ним идёт как к массиву AFR[0] - младшие восемь выводов (0-7), AFR[1] - старшие 8 (8-15). Теперь мы можем поморгать светодиодами, правда ещё без классов:
#include "STM32.h" // На моей плате светодиоды здесь. Правьте в соответсвии со схемой своей плыты #define LED_GPIO GPIOF #define LED_PIN GPIO_PIN_9 int main(void) { SystemClock_Config(Quartz_8); // Инициализируем тактовый генератор _SetPin(LED_GPIO, LED_PIN, Output, No_Pull); // Инициализируем порт и соответствующий вывод while (1) { _DigitalWriteBit(LED_GPIO, LED_PIN, High); // Переводим вывод в высокий уровень delay(500); // Задержка полсекунды _DigitalWriteBit(LED_GPIO, LED_PIN, Low); // Переводим вывод в низкий уровень delay(500); } }
Проект для данной статьи находится в каталоге WorkDevel\Developer\TestsMK\F407\F407VExx_GPIO. Светодиод должен мигать с периодичностью в одну секунду. В следующей статье начнём изучать классы.
Как всегда проекты и библиотеки на Яндекс Диске и в виде архива. Кроме этого, я добавил в проект WorkDevel\Developer\TestsMK\F407\F407VExx_micros вывод тактовой частоты на вывод одного из портов. У кого есть осцилограф до 50МГц, можно будет проверить.
Особая благодарность людям, благодаря которым я учусь:
- DiMoon - http://dimoon.ru/author/dim-siv
- Microsin - http://microsin.net/
- Hamper - https://www.rotr.info/index.htm
- MY Practic - http://mypractic.ru/uroki-stm32
Пока всё.
Тема на форуме - перейти.
Интересно, что дальше выйдет, буду следить за обновлениями.
Уже готово. Сейчас админу перешлю.